Package x2go :: Module sshproxy
[frames] | no frames]

Source Code for Module x2go.sshproxy

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Copyright (C) 2010-2012 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de> 
  4  # 
  5  # Python X2Go is free software; you can redistribute it and/or modify 
  6  # it under the terms of the GNU Affero General Public License as published by 
  7  # the Free Software Foundation; either version 3 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # Python X2Go 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 Affero General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU Affero General Public License 
 16  # along with this program; if not, write to the 
 17  # Free Software Foundation, Inc., 
 18  # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. 
 19   
 20  """\ 
 21  L{X2goSSHProxy} class - providing a forwarding tunnel for connecting to servers behind firewalls. 
 22   
 23  """ 
 24  __NAME__ = 'x2gosshproxy-pylib' 
 25   
 26  # modules 
 27  import gevent 
 28  import os 
 29  import copy 
 30  import paramiko 
 31  import threading 
 32   
 33  import string 
 34  import random 
 35   
 36  # Python X2Go modules 
 37  import forward 
 38  import checkhosts 
 39  import log 
 40  import utils 
 41  import x2go_exceptions 
 42   
 43  from x2go.defaults import CURRENT_LOCAL_USER as _CURRENT_LOCAL_USER 
 44  from x2go.defaults import LOCAL_HOME as _LOCAL_HOME 
 45  from x2go.defaults import X2GO_SSH_ROOTDIR as _X2GO_SSH_ROOTDIR 
 46   
 47  from monkey_patch_paramiko import monkey_patch_paramiko 
 48  monkey_patch_paramiko() 
 49   
50 -class X2goSSHProxy(paramiko.SSHClient, threading.Thread):
51 """\ 52 X2goSSHProxy can be used to proxy X2Go connections through a firewall via SSH. 53 54 """ 55 fw_tunnel = None 56
57 - def __init__(self, hostname=None, port=22, username=None, password=None, key_filename=None, 58 local_host='localhost', local_port=22022, remote_host='localhost', remote_port=22, 59 known_hosts=None, add_to_known_hosts=False, pkey=None, 60 sshproxy_host=None, sshproxy_port=22, sshproxy_user=None, 61 sshproxy_password=None, sshproxy_key_filename=None, sshproxy_pkey=None, 62 sshproxy_tunnel=None, 63 ssh_rootdir=os.path.join(_LOCAL_HOME, _X2GO_SSH_ROOTDIR), 64 session_instance=None, 65 logger=None, loglevel=log.loglevel_DEFAULT, ):
66 """\ 67 Initialize an X2goSSHProxy instance. Use an instance of this class to tunnel X2Go requests through 68 a proxying SSH server (i.e. to subLANs that are separated by firewalls or to private IP subLANs that 69 are NATted behind routers). 70 71 @param username: login user name to be used on the SSH proxy host 72 @type username: C{str} 73 @param password: user's password on the SSH proxy host, with private key authentication it will be 74 used to unlock the key (if needed) 75 @type password: C{str} 76 @param key_filename: name of a SSH private key file 77 @type key_filename: C{str} 78 @param pkey: a private DSA/RSA key object (as provided by Paramiko/SSH) 79 @type pkey: C{RSA/DSA key instance} 80 @param local_host: bind SSH tunnel to the C{local_host} IP socket address (default: localhost) 81 @type local_host: C{str} 82 @param local_port: IP socket port to bind the SSH tunnel to (default; 22022) 83 @type local_port: C{int} 84 @param remote_host: remote endpoint of the SSH proxying/forwarding tunnel (default: localhost) 85 @type remote_host: C{str} 86 @param remote_port: remote endpoint's IP socket port for listening SSH daemon (default: 22) 87 @type remote_port: C{int} 88 @param known_hosts: full path to a custom C{known_hosts} file 89 @type known_hosts: C{str} 90 @param add_to_known_hosts: automatically add host keys of unknown SSH hosts to the C{known_hosts} file 91 @type add_to_known_hosts: C{bool} 92 @param hostname: alias for C{local_host} 93 @type hostname: C{str} 94 @param port: alias for C{local_port} 95 @type port: C{int} 96 @param sshproxy_host: alias for C{remote_host} 97 @type sshproxy_host: C{str} 98 @param sshproxy_port: alias for C{remote_port} 99 @type sshproxy_port: C{int} 100 @param sshproxy_user: alias for C{username} 101 @type sshproxy_user: C{str} 102 @param sshproxy_password: alias for C{password} 103 @type sshproxy_password: C{str} 104 @param sshproxy_key_filename: alias for C{key_filename} 105 @type sshproxy_key_filename: C{str} 106 @param sshproxy_pkey: alias for C{pkey} 107 @type sshproxy_pkey: C{RSA/DSA key instance} (Paramiko) 108 109 @param sshproxy_tunnel: a string of the format <local_host>:<local_port>:<remote_host>:<remote_port> 110 which will override---if used---the options: C{local_host}, C{local_port}, C{remote_host} and C{remote_port} 111 @type sshproxy_tunnel: C{str} 112 113 @param ssh_rootdir: local user's SSH base directory (default: ~/.ssh) 114 @type ssh_rootdir: C{str} 115 @param session_instance: the L{X2goSession} instance that builds up this SSH proxying tunnel 116 @type session_instance: L{X2goSession} instance 117 @param logger: you can pass an L{X2goLogger} object to the 118 L{X2goSSHProxy} constructor 119 @type logger: L{X2goLogger} instance 120 @param loglevel: if no L{X2goLogger} object has been supplied a new one will be 121 constructed with the given loglevel 122 @type loglevel: int 123 124 @raise X2goSSHProxyAuthenticationException: if the SSH proxy caused a C{paramiko.AuthenticationException} 125 @raise X2goSSHProxyException: if the SSH proxy caused a C{paramiko.SSHException} 126 """ 127 if logger is None: 128 self.logger = log.X2goLogger(loglevel=loglevel) 129 else: 130 self.logger = copy.deepcopy(logger) 131 self.logger.tag = __NAME__ 132 133 self.hostname, self.port, self.username = hostname, port, username 134 135 # translate between X2goSession options and paramiko.SSHCLient.connect() options 136 if sshproxy_host: 137 if sshproxy_host.find(':'): 138 self.hostname = sshproxy_host.split(':')[0] 139 try: self.port = int(sshproxy_host.split(':')[1]) 140 except IndexError: pass 141 else: 142 self.hostname = sshproxy_host 143 144 if sshproxy_user: self.username = sshproxy_user 145 if sshproxy_password: password = sshproxy_password 146 if sshproxy_key_filename: key_filename = sshproxy_key_filename 147 if sshproxy_pkey: pkey = sshproxy_pkey 148 if sshproxy_tunnel: 149 self.local_host, self.local_port, self.remote_host, self.remote_port = sshproxy_tunnel.split(':') 150 self.local_port = int(self.local_port) 151 self.remote_port = int(self.remote_port) 152 else: 153 self.local_host = local_host 154 self.local_port = int(local_port) 155 self.remote_host = remote_host 156 self.remote_port = int(remote_port) 157 158 # allow more trailing whitespace tolerance in hostnames 159 self.hostname = self.hostname.strip() 160 self.local_host = self.local_host.strip() 161 self.remote_host = self.remote_host.strip() 162 163 # enforce IPv4 for localhost addresses!!! 164 _hostname = self.hostname 165 if _hostname in ('localhost', 'localhost.localdomain'): 166 _hostname = '127.0.0.1' 167 if self.local_host in ('localhost', 'localhost.localdomain'): 168 self.local_host = '127.0.0.1' 169 if self.remote_host in ('localhost', 'localhost.localdomain'): 170 self.remote_host = '127.0.0.1' 171 172 if username is None: 173 username = _CURRENT_LOCAL_USER 174 175 self._keepalive = True 176 self.session_instance = session_instance 177 178 self.client_instance = None 179 if self.session_instance is not None: 180 self.client_instance = self.session_instance.get_client_instance() 181 182 self.ssh_rootdir = ssh_rootdir 183 paramiko.SSHClient.__init__(self) 184 185 self.known_hosts = known_hosts 186 if self.known_hosts: 187 utils.touch_file(self.known_hosts) 188 self.load_host_keys(self.known_hosts) 189 190 if not add_to_known_hosts and session_instance: 191 self.set_missing_host_key_policy(checkhosts.X2goInteractiveAddPolicy(caller=self, session_instance=session_instance)) 192 193 if add_to_known_hosts: 194 self.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 195 196 try: 197 if (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey: 198 try: 199 if password: 200 self.connect(_hostname, port=self.port, 201 username=self.username, 202 password=password, 203 key_filename=key_filename, 204 pkey=pkey, 205 look_for_keys=False, 206 allow_agent=False, 207 ) 208 else: 209 self.connect(_hostname, port=self.port, 210 username=self.username, 211 key_filename=key_filename, 212 pkey=pkey, 213 look_for_keys=False, 214 allow_agent=False, 215 ) 216 217 except x2go_exceptions.AuthenticationException, e: 218 self.close() 219 raise x2go_exceptions.X2goSSHProxyAuthenticationException('pubkey auth mechanisms both failed') 220 except: 221 self.close() 222 raise 223 224 # since Paramiko 1.7.7.1 there is compression available, let's use it if present... 225 t = self.get_transport() 226 if hasattr(t, 'use_compression'): 227 t.use_compression(compress=True) 228 229 # if there is not private key, we will use the given password, if any 230 else: 231 # create a random password if password is empty to trigger host key validity check 232 if not password: 233 password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)]) 234 try: 235 self.connect(_hostname, port=self.port, 236 username=self.username, 237 password=password, 238 look_for_keys=False, 239 allow_agent=False, 240 ) 241 except x2go_exceptions.AuthenticationException: 242 self.close() 243 raise x2go_exceptions.X2goSSHProxyAuthenticationException('interactive auth mechanisms failed') 244 except: 245 self.close() 246 raise 247 248 except x2go_exceptions.SSHException, e: 249 self.close() 250 raise x2go_exceptions.X2goSSHProxyException(str(e)) 251 except: 252 self.close() 253 raise 254 255 self.set_missing_host_key_policy(paramiko.RejectPolicy()) 256 threading.Thread.__init__(self) 257 self.daemon = True
258
259 - def check_host(self):
260 """\ 261 Wraps around a Paramiko/SSH host key check. 262 263 """ 264 # hostname rewrite for localhost, force to IPv4 265 _hostname = self.hostname 266 267 # force into IPv4 for localhost connections 268 if _hostname in ('localhost', 'localhost.localdomain'): 269 _hostname = '127.0.0.1' 270 271 _valid = False 272 (_valid, _hostname, _port, _fingerprint, _fingerprint_type) = checkhosts.check_ssh_host_key(self, _hostname, port=self.port) 273 if not _valid and self.session_instance: 274 _valid = self.session_instance.HOOK_check_host_dialog(_hostname, _port, fingerprint=_fingerprint, fingerprint_type=_fingerprint_type) 275 return _valid
276
277 - def run(self):
278 """\ 279 Start the SSH proxying tunnel... 280 281 @raise X2goSSHProxyException: if the SSH proxy could not retrieve an SSH transport for proxying a X2Go server-client connection 282 283 """ 284 if self.get_transport() is not None and self.get_transport().is_authenticated(): 285 self.local_port = utils.detect_unused_port(bind_address=self.local_host, preferred_port=self.local_port) 286 if self.client_instance is not None: 287 _profile_id = self.session_instance.get_profile_id() 288 if self.client_instance.session_profiles.has_profile(_profile_id): 289 self.client_instance.session_profiles.update_value(_profile_id, 290 'sshproxytunnel', 291 '%s:%s:%s:%s' % (self.local_host, self.local_port, self.remote_host, self.remote_port) 292 ) 293 self.client_instance.session_profiles.write_user_config = True 294 self.client_instance.session_profiles.write() 295 self.fw_tunnel = forward.start_forward_tunnel(local_host=self.local_host, 296 local_port=self.local_port, 297 remote_host=self.remote_host, 298 remote_port=self.remote_port, 299 ssh_transport=self.get_transport(), 300 logger=self.logger, ) 301 self.logger('SSH proxy tunnel via [%s]:%s has been set up' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE) 302 self.logger('SSH proxy tunnel startpoint is [%s]:%s, endpoint is [%s]:%s' % (self.local_host, self.local_port, self.remote_host, self.remote_port), loglevel=log.loglevel_NOTICE) 303 304 while self._keepalive: 305 gevent.sleep(.1) 306 307 else: 308 raise x2go_exceptions.X2goSSHProxyException('SSH proxy connection could not retrieve an SSH transport')
309
310 - def get_local_proxy_port(self):
311 """\ 312 Retrieve the local IP socket port this SSH proxying tunnel is (about to) bind/bound to. 313 314 @return: local IP socket port 315 @rtype: C{int} 316 317 """ 318 return self.local_port
319
320 - def stop_thread(self):
321 """\ 322 Tear down the SSH proxying tunnel. 323 324 """ 325 if self.fw_tunnel is not None and self.fw_tunnel.is_active: 326 self.logger('taking down SSH proxy tunnel via [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE) 327 try: forward.stop_forward_tunnel(self.fw_tunnel) 328 except: pass 329 self.fw_tunnel = None 330 self._keepalive = False 331 if self.get_transport() is not None: 332 self.logger('closing SSH proxy connection to [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE) 333 self.close() 334 self.password = self.sshproxy_password = None
335
336 - def __del__(self):
337 """\ 338 Class desctructor. 339 340 """ 341 self.stop_thread()
342