diff --git a/src/common/connection.py b/src/common/connection.py index 7154a56bc..d191c8237 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -424,10 +424,8 @@ class Connection(ConnectionHandlers): use_srv = gajim.config.get_per('accounts', self.name, 'use_srv') use_custom = gajim.config.get_per('accounts', self.name, 'use_custom_host') - print 'use_custom = %s' % use_custom custom_h = gajim.config.get_per('accounts', self.name, 'custom_host') custom_p = gajim.config.get_per('accounts', self.name, 'custom_port') - print 'custom_port = %s' % custom_p # create connection if it doesn't already exist self.connected = 1 @@ -519,9 +517,11 @@ class Connection(ConnectionHandlers): if self._proxy and self._proxy['type']=='bosh': # with BOSH, we can't do TLS negotiation with , we do only "plain" # connection and TLS with handshake right after TCP connecting ("ssl") - try: - self._connection_types.remove('tls') - except ValueError: pass + scheme = common.xmpp.transports_nb.urisplit(self._proxy['bosh_uri'])[0] + if scheme=='https': + self._connection_types = ['ssl'] + else: + self._connection_types = ['plain'] host = self.select_next_host(self._hosts) self._current_host = host @@ -553,7 +553,7 @@ class Connection(ConnectionHandlers): if self._current_type == 'ssl': # SSL (force TLS on different port than plain) # If we do TLS over BOSH, port of XMPP server should be the standard one - # and TLS should be negotiated because immediate TLS on 5223 is deprecated + # and TLS should be negotiated because TLS on 5223 is deprecated if self._proxy and self._proxy['type']=='bosh': port = self._current_host['port'] else: @@ -583,7 +583,6 @@ class Connection(ConnectionHandlers): log.info('Connecting to %s: [%s:%d]', self.name, self._current_host['host'], port) - print secure_tuple con.connect( hostname=self._current_host['host'], port=port, @@ -1303,6 +1302,7 @@ class Connection(ConnectionHandlers): self.connect(config) def _on_new_account(self, con = None, con_type = None): + print 'on_new_acc- con: %s, con_type: %s' % (con, con_type) if not con_type: self.dispatch('NEW_ACC_NOT_CONNECTED', (_('Could not connect to "%s"') % self._hostname)) diff --git a/src/common/xmpp/__init__.py b/src/common/xmpp/__init__.py index 109b9b4c0..19c13bfa6 100644 --- a/src/common/xmpp/__init__.py +++ b/src/common/xmpp/__init__.py @@ -27,7 +27,7 @@ and use only methods for access all values you should not have any problems. """ import simplexml, protocol, auth_nb, transports_nb, roster_nb -import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb +import dispatcher_nb, features_nb, idlequeue, bosh, tls_nb, proxy_connectors from client_nb import * from client import * from protocol import * diff --git a/src/common/xmpp/auth_nb.py b/src/common/xmpp/auth_nb.py index 783bfbc24..f4ca60058 100644 --- a/src/common/xmpp/auth_nb.py +++ b/src/common/xmpp/auth_nb.py @@ -253,8 +253,6 @@ class NonBlockingNonSASL(PlugIn): def plugin(self, owner): ''' Determine the best auth method (digest/0k/plain) and use it for auth. Returns used method name on success. Used internally. ''' - if not self.resource: - return self.authComponent(owner) log.info('Querying server about possible auth methods') self.owner = owner @@ -303,33 +301,6 @@ class NonBlockingNonSASL(PlugIn): log.error('Authentication failed!') return self.on_auth(None) - def authComponent(self,owner): - ''' Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. ''' - self.handshake=0 - owner.send(Node(NS_COMPONENT_ACCEPT+' handshake', - payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()])) - owner.RegisterHandler('handshake', self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT) - self._owner.onreceive(self._on_auth_component) - - def _on_auth_component(self, data): - ''' called when we receive some response, after we send the handshake ''' - if data: - self.Dispatcher.ProcessNonBlocking(data) - if not self.handshake: - log.info('waiting on handshake') - return - self._owner.onreceive(None) - owner._registered_name=self.user - if self.handshake+1: - return self.on_auth('ok') - self.on_auth(None) - - def handshakeHandler(self,disp,stanza): - ''' Handler for registering in dispatcher for accepting transport authentication. ''' - if stanza.getName() == 'handshake': - self.handshake=1 - else: - self.handshake=-1 class NonBlockingBind(PlugIn): ''' Bind some JID to the current connection to allow router know of our location.''' diff --git a/src/common/xmpp/bosh.py b/src/common/xmpp/bosh.py index 2c8ca9f4d..5d1796a99 100644 --- a/src/common/xmpp/bosh.py +++ b/src/common/xmpp/bosh.py @@ -1,11 +1,29 @@ +## bosh.py +## +## +## Copyright (C) 2008 Tomas Karasek +## +## This file is part of Gajim. +## +## Gajim 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; version 3 only. +## +## Gajim 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. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . -import locale, random + +import locale, random, sha from transports_nb import NonBlockingTransport, NonBlockingHTTPBOSH,\ CONNECTED, CONNECTING, DISCONNECTED, DISCONNECTING,\ - urisplit + urisplit, DISCONNECT_TIMEOUT_SECONDS from protocol import BOSHBody from simplexml import Node -import sha import logging log = logging.getLogger('gajim.c.x.bosh') @@ -62,6 +80,13 @@ class NonBlockingBOSH(NonBlockingTransport): self.key_stack = None self.ack_checker = None self.after_init = False + self.proxy_dict = {} + if self.over_proxy and self.estabilish_tls: + self.proxy_dict['type'] = 'http' + # with SSL over proxy, we do HTTP CONNECT to proxy to open a channel to + # BOSH Connection Manager + self.proxy_dict['xmpp_server'] = (urisplit(self.bosh_uri)[1], self.bosh_port) + self.proxy_dict['credentials'] = self.proxy_creds def connect(self, conn_5tuple, on_connect, on_connect_failure): @@ -81,13 +106,9 @@ class NonBlockingBOSH(NonBlockingTransport): self.http_socks.append(self.get_new_http_socket()) self.tcp_connecting_started() - # following connect() is not necessary because sockets can be connected on - # send but we need to know if host is reachable in order to invoke callback - # for connecting failure eventually (the callback is different than callback - # for errors occurring after connection is etabilished) self.http_socks[0].connect( conn_5tuple = conn_5tuple, - on_connect = lambda: self._on_connect(), + on_connect = self._on_connect, on_connect_failure = self._on_connect_failure) def _on_connect(self): @@ -98,28 +119,29 @@ class NonBlockingBOSH(NonBlockingTransport): def set_timeout(self, timeout): - if self.get_state() in [CONNECTING, CONNECTED] and self.fd != -1: + if self.get_state() != DISCONNECTED and self.fd != -1: NonBlockingTransport.set_timeout(self, timeout) else: log.warn('set_timeout: TIMEOUT NOT SET: state is %s, fd is %s' % (self.get_state(), self.fd)) def on_http_request_possible(self): ''' - Called after HTTP response is received - another request is possible. - There should be always one pending request on BOSH CM. + Called after HTTP response is received - when another request is possible. + There should be always one pending request to BOSH CM. ''' log.info('on_http_req possible, state:\n%s' % self.get_current_state()) - if self.get_state() == DISCONNECTING: - self.disconnect() - return + if self.get_state()==DISCONNECTED: return #Hack for making the non-secure warning dialog work - if hasattr(self._owner, 'NonBlockingNonSASL') or hasattr(self._owner, 'SASL'): - self.send_BOSH(None) + if self._owner.got_features: + if (hasattr(self._owner, 'NonBlockingNonSASL') or hasattr(self._owner, 'SASL')): + self.send_BOSH(None) + else: + return else: - self.http_socks[0]._plug_idle(writable=False, readable=True) - return + self.send_BOSH(None) + def get_socket_in(self, state): for s in self.http_socks: @@ -129,7 +151,6 @@ class NonBlockingBOSH(NonBlockingTransport): def get_free_socket(self): if self.http_pipelining: - assert( len(self.http_socks) == 1 ) return self.get_socket_in(CONNECTED) else: last_recv_time, tmpsock = 0, None @@ -184,11 +205,12 @@ class NonBlockingBOSH(NonBlockingTransport): # CONNECTED with too many pending requests s = self.get_socket_in(DISCONNECTED) - # if we have DISCONNECTED socket, lets connect it and ... + # if we have DISCONNECTED socket, lets connect it and plug for send if s: self.connect_and_flush(s) else: - if len(self.http_socks) > 1: return + #if len(self.http_socks) > 1: return + print 'connecting sock' ss = self.get_new_http_socket() self.http_socks.append(ss) self.connect_and_flush(ss) @@ -200,7 +222,7 @@ class NonBlockingBOSH(NonBlockingTransport): if s: s._plug_idle(writable=True, readable=True) else: - log.error('=====!!!!!!!!====> Couldnt get free socket in plug_socket())') + log.error('=====!!!!!!!!====> Couldn\'t get free socket in plug_socket())') def build_stanza(self, socket): if self.prio_bosh_stanzas: @@ -222,21 +244,20 @@ class NonBlockingBOSH(NonBlockingTransport): log.info('sending msg with rid=%s to sock %s' % (stanza.getAttr('rid'), id(socket))) - socket.send(stanza) - self.renew_bosh_wait_timeout() + #socket.send(stanza) + self.renew_bosh_wait_timeout(self.bosh_wait + 3) return stanza def on_bosh_wait_timeout(self): - log.error('Connection Manager didn\'t respond within % seconds --> forcing \ - disconnect' % self.bosh_wait) + log.error('Connection Manager didn\'t respond within %s + 3 seconds --> forcing disconnect' % self.bosh_wait) self.disconnect() - def renew_bosh_wait_timeout(self): + def renew_bosh_wait_timeout(self, timeout): if self.wait_cb_time is not None: self.remove_bosh_wait_timeout() - sched_time = self.idlequeue.set_alarm(self.on_bosh_wait_timeout, self.bosh_wait+10) + sched_time = self.idlequeue.set_alarm(self.on_bosh_wait_timeout, timeout) self.wait_cb_time = sched_time def remove_bosh_wait_timeout(self): @@ -244,10 +265,17 @@ class NonBlockingBOSH(NonBlockingTransport): self.on_bosh_wait_timeout, self.wait_cb_time) - def on_persistent_fallback(self): + def on_persistent_fallback(self, socket): log.warn('Fallback to nonpersistent HTTP (no pipelining as well)') - self.http_persistent = False - self.http_pipelining = False + if socket.http_persistent: + socket.http_persistent = False + self.http_persistent = False + self.http_pipelining = False + socket.disconnect(do_callback=False) + self.connect_and_flush(socket) + else: + socket.disconnect() + def handle_body_attrs(self, stanza_attrs): @@ -277,7 +305,8 @@ class NonBlockingBOSH(NonBlockingTransport): if stanza_attrs.has_key('condition'): condition = stanza_attrs['condition'] log.error('Received terminating stanza: %s - %s' % (condition, bosh_errors[condition])) - self.set_state(DISCONNECTING) + self.disconnect() + return if stanza_attrs['type'] == 'error': # recoverable error @@ -295,8 +324,6 @@ class NonBlockingBOSH(NonBlockingTransport): def send(self, stanza, now=False): - # body tags should be send only via send_BOSH() - assert(not isinstance(stanza, BOSHBody)) self.send_BOSH(stanza) @@ -350,6 +377,7 @@ class NonBlockingBOSH(NonBlockingTransport): def start_disconnect(self): NonBlockingTransport.start_disconnect(self) + self.renew_bosh_wait_timeout(DISCONNECT_TIMEOUT_SECONDS) self.send_BOSH( (BOSHBody(attrs={'sid': self.bosh_sid, 'type': 'terminate'}), True)) @@ -359,7 +387,7 @@ class NonBlockingBOSH(NonBlockingTransport): 'http_port': self.bosh_port, 'http_version': self.http_version, 'http_persistent': self.http_persistent, - 'over_proxy': self.over_proxy} + 'add_proxy_headers': self.over_proxy and not self.estabilish_tls} if self.use_proxy_auth: http_dict['proxy_user'], http_dict['proxy_pass'] = self.proxy_creds @@ -372,6 +400,7 @@ class NonBlockingBOSH(NonBlockingTransport): certs = self.certs, on_http_request_possible = self.on_http_request_possible, http_dict = http_dict, + proxy_dict = self.proxy_dict, on_persistent_fallback = self.on_persistent_fallback) s.onreceive(self.on_received_http) s.set_stanza_build_cb(self.build_stanza) diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 9ecd46c83..35d5079f4 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -56,12 +56,12 @@ class NBCommonClient: self._owner = self self._registered_name = None self.connected = '' - self._component=0 self.socket = None self.on_connect = None self.on_proxy_failure = None self.on_connect_failure = None self.proxy = None + self.got_features = False def on_disconnect(self): @@ -72,7 +72,6 @@ class NBCommonClient: ''' self.connected='' - log.debug('Client disconnected..') for i in reversed(self.disconnect_handlers): log.debug('Calling disconnect handler %s' % i) i() @@ -84,18 +83,13 @@ class NBCommonClient: self.NonBlockingNonSASL.PlugOut() if self.__dict__.has_key('SASL'): self.SASL.PlugOut() - if self.__dict__.has_key('NonBlockingTLS'): - self.NonBlockingTLS.PlugOut() - if self.__dict__.has_key('NBHTTPProxySocket'): - self.NBHTTPProxySocket.PlugOut() - if self.__dict__.has_key('NBSOCKS5ProxySocket'): - self.NBSOCKS5ProxySocket.PlugOut() if self.__dict__.has_key('NonBlockingTCP'): self.NonBlockingTCP.PlugOut() if self.__dict__.has_key('NonBlockingHTTP'): self.NonBlockingHTTP.PlugOut() if self.__dict__.has_key('NonBlockingBOSH'): self.NonBlockingBOSH.PlugOut() + log.debug('Client disconnected..') def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, @@ -181,7 +175,9 @@ class NBCommonClient: if not mode: # starting state - if self.__dict__.has_key('Dispatcher'): self.Dispatcher.PlugOut() + if self.__dict__.has_key('Dispatcher'): + self.Dispatcher.PlugOut() + self.got_features = False d=dispatcher_nb.Dispatcher().PlugIn(self) on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') @@ -197,7 +193,7 @@ class NBCommonClient: mode='FAILURE', data='Error on stream open') if self.incoming_stream_version() == '1.0': - if not self.Dispatcher.Stream.features: + if not self.got_features: on_next_receive('RECEIVE_STREAM_FEATURES') else: log.info('got STREAM FEATURES in first recv') @@ -212,7 +208,7 @@ class NBCommonClient: # sometimes are received together with document # attributes and sometimes on next receive... self.Dispatcher.ProcessNonBlocking(data) - if not self.Dispatcher.Stream.features: + if not self.got_features: self._xmpp_connect_machine( mode='FAILURE', data='Missing in 1.0 stream') @@ -263,10 +259,6 @@ class NBCommonClient: self.on_connect_failure(retry) def _on_connect(self): - if self.secure == 'tls': - self._on_connect_failure('uaaaaaa') - return - print 'self.secure = %s' % self.secure self.onreceive(None) self.on_connect(self, self.connected) @@ -343,7 +335,6 @@ class NBCommonClient: # wrong user/pass, stop auth self.connected = None self._on_sasl_auth(None) - self.SASL.PlugOut() elif self.SASL.startsasl == 'success': auth_nb.NonBlockingBind().PlugIn(self) if self.protocol_type == 'BOSH': @@ -418,6 +409,9 @@ class NonBlockingClient(NBCommonClient): certs = (self.cacerts, self.mycerts) self._on_tcp_failure = self._on_connect_failure + proxy_dict = {} + tcp_host=xmpp_hostname + tcp_port=self.Port if proxy: # with proxies, client connects to proxy instead of directly to @@ -444,27 +438,18 @@ class NonBlockingClient(NBCommonClient): else: self._on_tcp_failure = self.on_proxy_failure - if proxy['type'] == 'socks5': - proxy_class = transports_nb.NBSOCKS5ProxySocket - elif proxy['type'] == 'http': - proxy_class = transports_nb.NBHTTPProxySocket - self.socket = proxy_class( - on_disconnect = self.on_disconnect, - raise_event = self.raise_event, - idlequeue = self.idlequeue, - estabilish_tls = estabilish_tls, - certs = certs, - proxy_creds = (proxy_user, proxy_pass), - xmpp_server = (xmpp_hostname, self.Port)) - else: - tcp_host=xmpp_hostname - tcp_port=self.Port + proxy_dict['type'] = proxy['type'] + proxy_dict['xmpp_server'] = (xmpp_hostname, self.Port) + proxy_dict['credentials'] = (proxy_user, proxy_pass) + + if not proxy or proxy['type'] != 'bosh': self.socket = transports_nb.NonBlockingTCP( on_disconnect = self.on_disconnect, raise_event = self.raise_event, idlequeue = self.idlequeue, estabilish_tls = estabilish_tls, - certs = certs) + certs = certs, + proxy_dict = proxy_dict) self.socket.PlugIn(self) diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 5b1eb0be4..430ff534d 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -305,6 +305,7 @@ class XMPPDispatcher(PlugIn): name = stanza.getName() if name=='features': + self._owner.got_features = True session.Stream.features=stanza xmlns=stanza.getNamespace() @@ -390,7 +391,7 @@ class XMPPDispatcher(PlugIn): ''' Put stanza on the wire and wait for recipient's response to it. ''' if timeout is None: timeout = DEFAULT_TIMEOUT_SECONDS - self._witid = self._owner.send(stanza) + self._witid = self.send(stanza) if func: self.on_responses[self._witid] = (func, args) if timeout: diff --git a/src/common/xmpp/proxy_connectors.py b/src/common/xmpp/proxy_connectors.py new file mode 100644 index 000000000..1b769288a --- /dev/null +++ b/src/common/xmpp/proxy_connectors.py @@ -0,0 +1,221 @@ +## proxy_connectors.py +## based on transports_nb.py +## +## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov +## modified by Dimitur Kirov +## modified by Tomas Karasek +## +## 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. +import struct, socket, base64 + +''' +Module containing classes for proxy connecting. So far its HTTP CONNECT +and SOCKS5 proxy. +''' + +import logging +log = logging.getLogger('gajim.c.x.proxy_connectors') + +class ProxyConnector: + ''' + Interface for proxy-connecting object - when tunnneling XMPP over proxies, + some connecting process usually has to be done before opening stream. + Proxy connectors are used right after TCP connection is estabilished. + ''' + def __init__(self, send_method, onreceive, old_on_receive, on_success, + on_failure, xmpp_server, proxy_creds=(None,None)): + + self.send = send_method + self.onreceive = onreceive + self.old_on_receive = old_on_receive + self.on_success = on_success + self.on_failure = on_failure + self.xmpp_server = xmpp_server + self.proxy_user, self.proxy_pass = proxy_creds + self.old_on_receive = old_on_receive + + self.start_connecting() + + def start_connecting(self): + raise NotImplementedException() + + def connecting_over(self): + self.onreceive(self.old_on_receive) + self.on_success() + +class HTTPCONNECTConnector(ProxyConnector): + def start_connecting(self): + ''' + Connects to proxy, supplies login and password to it + (if were specified while creating instance). Instructs proxy to make + connection to the target server. + ''' + log.info('Proxy server contacted, performing authentification') + connector = ['CONNECT %s:%s HTTP/1.1' % self.xmpp_server, + 'Proxy-Connection: Keep-Alive', + 'Pragma: no-cache', + 'Host: %s:%s' % self.xmpp_server, + 'User-Agent: Gajim'] + if self.proxy_user and self.proxy_pass: + credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) + credentials = base64.encodestring(credentials).strip() + connector.append('Proxy-Authorization: Basic '+credentials) + connector.append('\r\n') + self.onreceive(self._on_headers_sent) + self.send('\r\n'.join(connector)) + + def _on_headers_sent(self, reply): + if reply is None: + return + self.reply = reply.replace('\r', '') + try: + proto, code, desc = reply.split('\n')[0].split(' ', 2) + except: + log.error("_on_headers_sent:", exc_info=True) + #traceback.print_exc() + self.on_failure('Invalid proxy reply') + return + if code <> '200': + log.error('Invalid proxy reply: %s %s %s' % (proto, code, desc)) + self.on_failure('Invalid proxy reply') + return + if len(reply) != 2: + pass + self.connecting_over() + + + +class SOCKS5Connector(ProxyConnector): + ''' + SOCKS5 proxy connection class. Allows to use SOCKS5 proxies with + (optionally) simple authentication (only USERNAME/PASSWORD auth). + ''' + def start_connecting(self): + log.info('Proxy server contacted, performing authentification') + if self.proxy_user and self.proxy_pass: + to_send = '\x05\x02\x00\x02' + else: + to_send = '\x05\x01\x00' + self.onreceive(self._on_greeting_sent) + self.send(to_send) + + def _on_greeting_sent(self, reply): + if reply is None: + return + if len(reply) != 2: + self.on_failure('Invalid proxy reply') + return + if reply[0] != '\x05': + log.info('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[1] == '\x00': + return self._on_proxy_auth('\x01\x00') + elif reply[1] == '\x02': + to_send = '\x01' + chr(len(self.proxy_user)) + self.proxy_user +\ + chr(len(self.proxy_pass)) + self.proxy_pass + self.onreceive(self._on_proxy_auth) + self.send(to_send) + else: + if reply[1] == '\xff': + log.error('Authentification to proxy impossible: no acceptable ' + 'auth method') + self.on_failure('Authentification to proxy impossible: no ' + 'acceptable authentification method') + return + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + + def _on_proxy_auth(self, reply): + if reply is None: + return + if len(reply) != 2: + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[0] != '\x01': + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[1] != '\x00': + log.error('Authentification to proxy failed') + self.on_failure('Authentification to proxy failed') + return + log.info('Authentification successfull. Jabber server contacted.') + # Request connection + req = "\x05\x01\x00" + # If the given destination address is an IP address, we'll + # use the IPv4 address request even if remote resolving was specified. + try: + self.ipaddr = socket.inet_aton(self.xmpp_server[0]) + req = req + "\x01" + self.ipaddr + except socket.error: + # Well it's not an IP number, so it's probably a DNS name. +# if self.__proxy[3]==True: + # Resolve remotely + self.ipaddr = None + req = req + "\x03" + chr(len(self.xmpp_server[0])) + self.xmpp_server[0] +# else: +# # Resolve locally +# self.ipaddr = socket.inet_aton(socket.gethostbyname(self.xmpp_server[0])) +# req = req + "\x01" + ipaddr + req = req + struct.pack(">H",self.xmpp_server[1]) + self.onreceive(self._on_req_sent) + self.send(req) + + def _on_req_sent(self, reply): + if reply is None: + return + if len(reply) < 10: + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[0] != '\x05': + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + if reply[1] != "\x00": + # Connection failed + if ord(reply[1])<9: + errors = ['general SOCKS server failure', + 'connection not allowed by ruleset', + 'Network unreachable', + 'Host unreachable', + 'Connection refused', + 'TTL expired', + 'Command not supported', + 'Address type not supported' + ] + txt = errors[ord(reply[1])-1] + else: + txt = 'Invalid proxy reply' + log.error(txt) + self.on_failure(txt) + return + # Get the bound address/port + elif reply[3] == "\x01": + begin, end = 3, 7 + elif reply[3] == "\x03": + begin, end = 4, 4 + reply[4] + else: + log.error('Invalid proxy reply') + self.on_failure('Invalid proxy reply') + return + self.connecting_over() + + + + + + + + diff --git a/src/common/xmpp/tls_nb.py b/src/common/xmpp/tls_nb.py index 313edfc9b..72faab425 100644 --- a/src/common/xmpp/tls_nb.py +++ b/src/common/xmpp/tls_nb.py @@ -201,7 +201,7 @@ class StdlibSSLWrapper(SSLWrapper): try: return self.sslobj.read(bufsize) except socket.sslerror, e: - log.debug("Recv: Caught socket.sslerror:", exc_info=True) + log.debug("Recv: Caught socket.sslerror: " + repr(e), exc_info=True) if e.args[0] not in (socket.SSL_ERROR_WANT_READ, socket.SSL_ERROR_WANT_WRITE): raise SSLWrapper.Error(self.sock or self.sslobj, e) return None @@ -238,7 +238,6 @@ class NonBlockingTLS(PlugIn): ''' log.info('Starting TLS estabilishing') PlugIn.PlugIn(self, owner) - print 'inplugin' try: self._owner._plug_idle(writable=False, readable=False) res = self._startSSL() @@ -273,8 +272,17 @@ class NonBlockingTLS(PlugIn): def _startSSL(self): ''' Immediatedly switch socket to TLS mode. Used internally.''' log.debug("_startSSL called") - if USE_PYOPENSSL: return self._startSSL_pyOpenSSL() - else: return self._startSSL_stdlib() + + if USE_PYOPENSSL: result = self._startSSL_pyOpenSSL() + else: result = self._startSSL_stdlib() + + if result: + log.debug("Synchronous handshake completed") + self._owner._plug_idle(writable=True, readable=False) + return True + else: + return False + def _startSSL_pyOpenSSL(self): log.debug("_startSSL_pyOpenSSL called") @@ -328,9 +336,8 @@ class NonBlockingTLS(PlugIn): log.error('Error while TLS handshake: ', exc_info=True) return False tcpsock._sslObj.setblocking(False) - log.debug("Synchronous handshake completed") self._owner.ssl_lib = PYOPENSSL - return self._endSSL() + return True def _startSSL_stdlib(self): @@ -349,10 +356,6 @@ class NonBlockingTLS(PlugIn): log.error("Exception caught in _startSSL_stdlib:", exc_info=True) return False self._owner.ssl_lib = PYSTDLIB - return self._endSSL() - - def _endSSL(self): - self._owner._plug_idle(writable=True, readable=False) return True def _ssl_verify_callback(self, sslconn, cert, errnum, depth, ok): diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index e41db3a92..166dd2956 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -15,20 +15,21 @@ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. -import socket,base64 from simplexml import ustr from client import PlugIn from idlequeue import IdleObject from protocol import * +import proxy_connectors import tls_nb +import socket import sys import os import errno import time - import traceback +import base64 import logging log = logging.getLogger('gajim.c.x.transports_nb') @@ -66,7 +67,7 @@ def get_proxy_data_from_dict(proxy): CONNECT_TIMEOUT_SECONDS = 30 # how long to wait for a disconnect to complete -DISCONNECT_TIMEOUT_SECONDS = 10 +DISCONNECT_TIMEOUT_SECONDS =5 # size of the buffer which reads data from server # if lower, more stanzas will be fragmented and processed twice @@ -82,7 +83,7 @@ DISCONNECTING = 'DISCONNECTING' CONNECTING = 'CONNECTING' PROXY_CONNECTING = 'PROXY_CONNECTING' CONNECTED = 'CONNECTED' -STATES = [DISCONNECTED, DISCONNECTING, CONNECTING, PROXY_CONNECTING, CONNECTED] +STATES = [DISCONNECTED, CONNECTING, PROXY_CONNECTING, CONNECTED, DISCONNECTING] # transports have different arguments in constructor and same in connect() # method @@ -124,7 +125,7 @@ class NonBlockingTransport(PlugIn): ''' self.on_connect = on_connect self.on_connect_failure = on_connect_failure - (self.server, self.port) = conn_5tuple[4][:2] + self.server, self.port = conn_5tuple[4][:2] self.conn_5tuple = conn_5tuple @@ -213,7 +214,8 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): ''' Non-blocking TCP socket wrapper ''' - def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs): + def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, + proxy_dict=None): ''' Class constructor. ''' @@ -224,6 +226,8 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # bytes remained from the last send message self.sendbuff = '' + self.proxy_dict = proxy_dict + self.on_remote_disconnect = self.disconnect() def start_disconnect(self): @@ -288,12 +292,29 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # which will also remove read_timeouts for descriptor self._on_connect_failure('Exception while connecting to %s:%s - %s %s' % (self.server, self.port, errnum, errstr)) + + def _connect_to_proxy(self): + self.set_state(PROXY_CONNECTING) + if self.proxy_dict['type'] == 'socks5': + proxyclass = proxy_connectors.SOCKS5Connector + elif self.proxy_dict['type'] == 'http' : + proxyclass = proxy_connectors.HTTPCONNECTConnector + proxyclass( + send_method = self.send, + onreceive = self.onreceive, + old_on_receive = self.on_receive, + on_success = self._on_connect, + on_failure = self._on_connect_failure, + xmpp_server = self.proxy_dict['xmpp_server'], + proxy_creds = self.proxy_dict['credentials'] + ) + def _on_connect(self): - ''' with TCP socket, we have to remove send-timeout ''' - self.idlequeue.remove_timeout(self.fd) - self.peerhost = self._sock.getsockname() - print self.estabilish_tls + ''' + Preceeds invoking of on_connect callback. TCP connection is estabilished at + this time. + ''' if self.estabilish_tls: self.tls_init( on_succ = lambda: NonBlockingTransport._on_connect(self), @@ -320,7 +341,10 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if self.get_state()==CONNECTING: log.info('%s socket wrapper connected' % id(self)) - self._on_connect() + self.idlequeue.remove_timeout(self.fd) + self.peerhost = self._sock.getsockname() + if self.proxy_dict: self._connect_to_proxy() + else: self._on_connect() return self._do_send() @@ -330,7 +354,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): if self.get_state()==CONNECTING: self._on_connect_failure('Error during connect to %s:%s' % (self.server, self.port)) - else : + else: self.disconnect() def disconnect(self, do_callback=True): @@ -338,6 +362,7 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): return self.set_state(DISCONNECTED) self.idlequeue.unplug_idle(self.fd) + if self.__dict__.has_key('NonBlockingTLS'): self.NonBlockingTLS.PlugOut() try: self._sock.shutdown(socket.SHUT_RDWR) self._sock.close() @@ -401,8 +426,6 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): Plugged socket will always be watched for "error" event - in that case, pollend() is called. ''' - self.idlequeue.plug_idle(self, writable, readable) - log.info('Plugging fd %d, W:%s, R:%s' % (self.fd, writable, readable)) self.idlequeue.plug_idle(self, writable, readable) @@ -436,10 +459,6 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): def _do_receive(self): ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' - # Misc error signifying that we got disconnected - ERR_DISCONN = -2 - # code for unknown/other errors - ERR_OTHER = -3 received = None errnum = 0 errstr = 'No Error Set' @@ -448,35 +467,29 @@ class NonBlockingTCP(NonBlockingTransport, IdleObject): # get as many bites, as possible, but not more than RECV_BUFSIZE received = self._recv(RECV_BUFSIZE) except socket.error, (errnum, errstr): - # save exception number and message to errnum, errstr log.info("_do_receive: got %s:" % received , exc_info=True) except tls_nb.SSLWrapper.Error, e: - log.info("_do_receive, caugth SSL error: got %s:" % received , exc_info=True) - errnum = tls_nb.gattr(e, 'errno') or ERR_OTHER - errstr = tls_nb.gattr(e, 'exc_str') + log.info("_do_receive, caught SSL error, got %s:" % received , exc_info=True) + errnum, errstr = e.exc + + if (self.ssl_lib is None and received == '') or \ + (self.ssl_lib == tls_nb.PYSTDLIB and errnum == 8 ) or \ + (self.ssl_lib == tls_nb.PYOPENSSL and errnum == -1 ): + # 8 in stdlib: errstr == EOF occured in violation of protocol + # -1 in pyopenssl: errstr == Unexpected EOF + log.info("Disconnected by remote server: %s %s" % (errnum, errstr), exc_info=True) + self.on_remote_disconnect() + return - if received == '': - errnum = ERR_DISCONN - errstr = "Connection closed unexpectedly" - if errnum in (ERR_DISCONN, errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN): - # ECONNRESET - connection you are trying to access has been reset by the peer - # ENOTCONN - Transport endpoint is not connected - # ESHUTDOWN - shutdown(2) has been called on a socket to close down the - # sending end of the transmision, and then data was attempted to be sent - log.error("Connection to %s lost: %s %s" % ( self.server, errnum, errstr), exc_info=True) - if hasattr(self, 'on_remote_disconnect'): self.on_remote_disconnect() - else: self.disconnect() + if errnum: + log.error("Connection to %s:%s lost: %s %s" % ( self.server, self.port, errnum, errstr), exc_info=True) + self.disconnect() return + # this branch is for case of non-fatal SSL errors - None is returned from + # recv() but no errnum is set if received is None: - # because there are two types of TLS wrappers, the TLS plugin recv method - # returns None in case of error - if errnum != 0: - log.error("CConnection to %s lost: %s %s" % (self.server, errnum, errstr)) - self.disconnect() - return - received = '' return # we have received some bytes, stop the timeout! @@ -505,10 +518,10 @@ class NonBlockingHTTP(NonBlockingTCP): ''' def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, - on_http_request_possible, on_persistent_fallback, http_dict): + on_http_request_possible, on_persistent_fallback, http_dict, proxy_dict = None): NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, - estabilish_tls, certs) + estabilish_tls, certs, proxy_dict) self.http_protocol, self.http_host, self.http_path = urisplit(http_dict['http_uri']) if self.http_protocol is None: @@ -518,7 +531,7 @@ class NonBlockingHTTP(NonBlockingTCP): self.http_port = http_dict['http_port'] self.http_version = http_dict['http_version'] self.http_persistent = http_dict['http_persistent'] - self.over_proxy = http_dict['over_proxy'] + self.add_proxy_headers = http_dict['add_proxy_headers'] if http_dict.has_key('proxy_user') and http_dict.has_key('proxy_pass'): self.proxy_user, self.proxy_pass = http_dict['proxy_user'], http_dict['proxy_pass'] else: @@ -528,46 +541,39 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff = '' self.expected_length = 0 self.pending_requests = 0 - self.on_persistent_fallback = on_persistent_fallback self.on_http_request_possible = on_http_request_possible - self.just_responed = False self.last_recv_time = 0 + self.close_current_connection = False + self.on_remote_disconnect = lambda: on_persistent_fallback(self) - def send(self, raw_data, now=False): - NonBlockingTCP.send( - self, - self.build_http_message(raw_data), - now) + def http_send(self, raw_data, now=False): + self.send(self.build_http_message(raw_data), now) - def on_remote_disconnect(self): - log.warn('on_remote_disconnect called, http_persistent = %s' % self.http_persistent) - if self.http_persistent: - self.http_persistent = False - self.on_persistent_fallback() - self.disconnect(do_callback=False) - self.connect( - conn_5tuple = self.conn_5tuple, - on_connect = self.on_http_request_possible, - on_connect_failure = self.disconnect) - - else: - self.disconnect() - return - def _on_receive(self,data): '''Preceeds passing received data to owner class. Gets rid of HTTP headers and checks them.''' + if self.get_state() == PROXY_CONNECTING: + NonBlockingTCP._on_receive(self, data) + return if not self.recvbuff: # recvbuff empty - fresh HTTP message was received - statusline, headers, self.recvbuff = self.parse_http_message(data) + try: + statusline, headers, self.recvbuff = self.parse_http_message(data) + except ValueError: + self.disconnect() + return + if statusline[1] != '200': log.error('HTTP Error: %s %s' % (statusline[1], statusline[2])) self.disconnect() return self.expected_length = int(headers['Content-Length']) + if headers.has_key('Connection') and headers['Connection'].strip()=='close': + self.close_current_connection = True + else: - #sth in recvbuff - append currently received data to HTTP mess in buffer + #sth in recvbuff - append currently received data to HTTP msg in buffer self.recvbuff = '%s%s' % (self.recvbuff, data) if self.expected_length > len(self.recvbuff): @@ -583,9 +589,10 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff='' self.expected_length=0 - if not self.http_persistent: + if not self.http_persistent or self.close_current_connection: # not-persistent connections disconnect after response self.disconnect(do_callback = False) + self.close_current_connection = False self.last_recv_time = time.time() self.on_receive(data=httpbody, socket=self) self.on_http_request_possible() @@ -601,9 +608,10 @@ class NonBlockingHTTP(NonBlockingTCP): self.http_port, self.http_path) headers = ['%s %s %s' % (method, absolute_uri, self.http_version), 'Host: %s:%s' % (self.http_host, self.http_port), + 'User-Agent: Gajim', 'Content-Type: text/xml; charset=utf-8', 'Content-Length: %s' % len(str(httpbody))] - if self.over_proxy: + if self.add_proxy_headers: headers.append('Proxy-Connection: keep-alive') headers.append('Pragma: no-cache') if self.proxy_user and self.proxy_pass: @@ -646,6 +654,9 @@ class NonBlockingHTTPBOSH(NonBlockingHTTP): self.build_cb = build_cb def _do_send(self): + if self.state == PROXY_CONNECTING: + NonBlockingTCP._do_send(self) + return if not self.sendbuff: stanza = self.build_cb(socket=self) stanza = self.build_http_message(httpbody=stanza) @@ -669,225 +680,4 @@ class NonBlockingHTTPBOSH(NonBlockingHTTP): -class NBProxySocket(NonBlockingTCP): - ''' - Interface for proxy socket wrappers - when tunnneling XMPP over proxies, - some connecting process usually has to be done before opening stream. - ''' - def __init__(self, raise_event, on_disconnect, idlequeue, estabilish_tls, certs, - xmpp_server, proxy_creds=(None,None)): - - self.proxy_user, self.proxy_pass = proxy_creds - self.xmpp_server = xmpp_server - NonBlockingTCP.__init__(self, raise_event, on_disconnect, idlequeue, - estabilish_tls, certs) - - def _on_connect(self): - ''' - We're redefining _on_connect method to insert proxy-specific mechanism before - invoking the ssl connection and then client callback. All the proxy connecting - is done before XML stream is opened. - ''' - self.set_state(PROXY_CONNECTING) - self._on_tcp_connect() - - - def _on_tcp_connect(self): - '''to be implemented in each proxy socket wrapper''' - pass - - -class NBHTTPProxySocket(NBProxySocket): - ''' This class can be used instead of NonBlockingTCP - HTTP (CONNECT) proxy connection class. Allows to use HTTP proxies like squid with - (optionally) simple authentication (using login and password). - ''' - - def _on_tcp_connect(self): - ''' 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. ''' - log.info('Proxy server contacted, performing authentification') - connector = ['CONNECT %s:%s HTTP/1.0' % self.xmpp_server, - 'Proxy-Connection: Keep-Alive', - 'Pragma: no-cache', - 'Host: %s:%s' % self.xmpp_server, - 'User-Agent: Gajim'] - if self.proxy_user and self.proxy_pass: - credentials = '%s:%s' % (self.proxy_user, self.proxy_pass) - credentials = base64.encodestring(credentials).strip() - connector.append('Proxy-Authorization: Basic '+credentials) - connector.append('\r\n') - self.onreceive(self._on_headers_sent) - self.send('\r\n'.join(connector)) - - def _on_headers_sent(self, reply): - if reply is None: - return - self.reply = reply.replace('\r', '') - try: - proto, code, desc = reply.split('\n')[0].split(' ', 2) - except: - log.error("_on_headers_sent:", exc_info=True) - #traceback.print_exc() - self._on_connect_failure('Invalid proxy reply') - return - if code <> '200': - log.error('Invalid proxy reply: %s %s %s' % (proto, code, desc)) - self._on_connect_failure('Invalid proxy reply') - return - if len(reply) != 2: - pass - NonBlockingTCP._on_connect(self) - #self.onreceive(self._on_proxy_auth) - - def _on_proxy_auth(self, reply): - if self.reply.find('\n\n') == -1: - if reply is None: - self._on_connect_failure('Proxy authentification failed') - return - if reply.find('\n\n') == -1: - self.reply += reply.replace('\r', '') - self._on_connect_failure('Proxy authentification failed') - return - log.info('Authentification successfull. Jabber server contacted.') - self._on_connect(self) - - -class NBSOCKS5ProxySocket(NBProxySocket): - '''SOCKS5 proxy connection class. Uses TCPsocket as the base class - redefines only connect method. Allows to use SOCKS5 proxies with - (optionally) simple authentication (only USERNAME/PASSWORD auth). - ''' - # TODO: replace on_proxy_failure() with - # _on_connect_failure, at the end call _on_connect() - - def _on_tcp_connect(self): - log.info('Proxy server contacted, performing authentification') - if self.proxy.has_key('user') and self.proxy.has_key('password'): - to_send = '\x05\x02\x00\x02' - else: - to_send = '\x05\x01\x00' - self.onreceive(self._on_greeting_sent) - self.send(to_send) - - def _on_greeting_sent(self, reply): - if reply is None: - return - if len(reply) != 2: - self.on_proxy_failure('Invalid proxy reply') - return - if reply[0] != '\x05': - log.info('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[1] == '\x00': - return self._on_proxy_auth('\x01\x00') - elif reply[1] == '\x02': - to_send = '\x01' + chr(len(self.proxy['user'])) + self.proxy['user'] +\ - chr(len(self.proxy['password'])) + self.proxy['password'] - self.onreceive(self._on_proxy_auth) - self.send(to_send) - else: - if reply[1] == '\xff': - log.error('Authentification to proxy impossible: no acceptable ' - 'auth method') - self._owner.disconnected() - self.on_proxy_failure('Authentification to proxy impossible: no ' - 'acceptable authentification method') - return - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - - def _on_proxy_auth(self, reply): - if reply is None: - return - if len(reply) != 2: - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[0] != '\x01': - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[1] != '\x00': - log.error('Authentification to proxy failed') - self._owner.disconnected() - self.on_proxy_failure('Authentification to proxy failed') - return - log.info('Authentification successfull. Jabber server contacted.') - # Request connection - req = "\x05\x01\x00" - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. - try: - self.ipaddr = socket.inet_aton(self.server[0]) - req = req + "\x01" + self.ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. -# if self.__proxy[3]==True: - # Resolve remotely - self.ipaddr = None - req = req + "\x03" + chr(len(self.server[0])) + self.server[0] -# else: -# # Resolve locally -# self.ipaddr = socket.inet_aton(socket.gethostbyname(self.server[0])) -# req = req + "\x01" + ipaddr - req = req + struct.pack(">H",self.server[1]) - self.onreceive(self._on_req_sent) - self.send(req) - - def _on_req_sent(self, reply): - if reply is None: - return - if len(reply) < 10: - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[0] != '\x05': - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - if reply[1] != "\x00": - # Connection failed - self._owner.disconnected() - if ord(reply[1])<9: - errors = ['general SOCKS server failure', - 'connection not allowed by ruleset', - 'Network unreachable', - 'Host unreachable', - 'Connection refused', - 'TTL expired', - 'Command not supported', - 'Address type not supported' - ] - txt = errors[ord(reply[1])-1] - else: - txt = 'Invalid proxy reply' - log.error(txt) - self.on_proxy_failure(txt) - return - # Get the bound address/port - elif reply[3] == "\x01": - begin, end = 3, 7 - elif reply[3] == "\x03": - begin, end = 4, 4 + reply[4] - else: - log.error('Invalid proxy reply') - self._owner.disconnected() - self.on_proxy_failure('Invalid proxy reply') - return - - if self.on_connect_proxy: - self.on_connect_proxy() - - -