From a76c173816c2692ef62f91e56a507bc7810f2d8f Mon Sep 17 00:00:00 2001 From: tomk Date: Thu, 14 Aug 2008 21:48:43 +0000 Subject: [PATCH] - improved SSL connections with BOSH - SSL over HTTP proxy is possible now, Gajim will do HTTP CONNECT on proxy to reach the BOSH Conn manager and try to estabilish TLS (same as what firefox do when approaching HTTPS server via proxy) - moved proxy-connecting code to xmpp/proxy_connectors.py - debugged SOCKS5 proxy code - tested with Tigase server --- src/common/connection.py | 14 +- src/common/xmpp/__init__.py | 2 +- src/common/xmpp/auth_nb.py | 29 --- src/common/xmpp/bosh.py | 99 +++++--- src/common/xmpp/client_nb.py | 49 ++-- src/common/xmpp/dispatcher_nb.py | 3 +- src/common/xmpp/proxy_connectors.py | 221 +++++++++++++++++ src/common/xmpp/tls_nb.py | 23 +- src/common/xmpp/transports_nb.py | 370 ++++++---------------------- 9 files changed, 405 insertions(+), 405 deletions(-) create mode 100644 src/common/xmpp/proxy_connectors.py 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() - - -