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  import x2go._paramiko 
 48  x2go._paramiko.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, force_password_auth=False, 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, look_for_keys=False, allow_agent=False, 60 sshproxy_host=None, sshproxy_port=22, sshproxy_user=None, 61 sshproxy_password=None, sshproxy_force_password_auth=False, sshproxy_key_filename=None, sshproxy_pkey=None, 62 sshproxy_look_for_keys=False, sshproxy_allow_agent=False, 63 sshproxy_tunnel=None, 64 ssh_rootdir=os.path.join(_LOCAL_HOME, _X2GO_SSH_ROOTDIR), 65 session_instance=None, 66 logger=None, loglevel=log.loglevel_DEFAULT, ):
67 """\ 68 Initialize an X2goSSHProxy instance. Use an instance of this class to tunnel X2Go requests through 69 a proxying SSH server (i.e. to subLANs that are separated by firewalls or to private IP subLANs that 70 are NATted behind routers). 71 72 @param username: login user name to be used on the SSH proxy host 73 @type username: C{str} 74 @param password: user's password on the SSH proxy host, with private key authentication it will be 75 used to unlock the key (if needed) 76 @type password: C{str} 77 @param key_filename: name of a SSH private key file 78 @type key_filename: C{str} 79 @param pkey: a private DSA/RSA key object (as provided by Paramiko/SSH) 80 @type pkey: C{RSA/DSA key instance} 81 @param force_password_auth: enforce password authentication even if a key(file) is present 82 @type force_password_auth: C{bool} 83 @param look_for_keys: look for key files with standard names and try those if any can be found 84 @type look_for_keys: C{bool} 85 @param allow_agent: try authentication via a locally available SSH agent 86 @type allow_agent: C{bool} 87 @param local_host: bind SSH tunnel to the C{local_host} IP socket address (default: localhost) 88 @type local_host: C{str} 89 @param local_port: IP socket port to bind the SSH tunnel to (default; 22022) 90 @type local_port: C{int} 91 @param remote_host: remote endpoint of the SSH proxying/forwarding tunnel (default: localhost) 92 @type remote_host: C{str} 93 @param remote_port: remote endpoint's IP socket port for listening SSH daemon (default: 22) 94 @type remote_port: C{int} 95 @param known_hosts: full path to a custom C{known_hosts} file 96 @type known_hosts: C{str} 97 @param add_to_known_hosts: automatically add host keys of unknown SSH hosts to the C{known_hosts} file 98 @type add_to_known_hosts: C{bool} 99 @param hostname: alias for C{local_host} 100 @type hostname: C{str} 101 @param port: alias for C{local_port} 102 @type port: C{int} 103 @param sshproxy_host: alias for C{hostname} 104 @type sshproxy_host: C{str} 105 @param sshproxy_port: alias for C{post} 106 @type sshproxy_port: C{int} 107 @param sshproxy_user: alias for C{username} 108 @type sshproxy_user: C{str} 109 @param sshproxy_password: alias for C{password} 110 @type sshproxy_password: C{str} 111 @param sshproxy_key_filename: alias for C{key_filename} 112 @type sshproxy_key_filename: C{str} 113 @param sshproxy_pkey: alias for C{pkey} 114 @type sshproxy_pkey: C{RSA/DSA key instance} (Paramiko) 115 @param sshproxy_force_password_auth: alias for C{force_password_auth} 116 @type sshproxy_force_password_auth: C{bool} 117 @param sshproxy_look_for_keys: alias for C{look_for_keys} 118 @type sshproxy_look_for_keys: C{bool} 119 @param sshproxy_allow_agent: alias for C{allow_agent} 120 @type sshproxy_allow_agent: C{bool} 121 122 @param sshproxy_tunnel: a string of the format <local_host>:<local_port>:<remote_host>:<remote_port> 123 which will override---if used---the options: C{local_host}, C{local_port}, C{remote_host} and C{remote_port} 124 @type sshproxy_tunnel: C{str} 125 126 @param ssh_rootdir: local user's SSH base directory (default: ~/.ssh) 127 @type ssh_rootdir: C{str} 128 @param session_instance: the L{X2goSession} instance that builds up this SSH proxying tunnel 129 @type session_instance: L{X2goSession} instance 130 @param logger: you can pass an L{X2goLogger} object to the 131 L{X2goSSHProxy} constructor 132 @type logger: L{X2goLogger} instance 133 @param loglevel: if no L{X2goLogger} object has been supplied a new one will be 134 constructed with the given loglevel 135 @type loglevel: int 136 137 @raise X2goSSHProxyAuthenticationException: if the SSH proxy caused a C{paramiko.AuthenticationException} 138 @raise X2goSSHProxyException: if the SSH proxy caused a C{paramiko.SSHException} 139 """ 140 if logger is None: 141 self.logger = log.X2goLogger(loglevel=loglevel) 142 else: 143 self.logger = copy.deepcopy(logger) 144 self.logger.tag = __NAME__ 145 146 self.hostname, self.port, self.username = hostname, port, username 147 148 if sshproxy_port: self.port = sshproxy_port 149 150 # translate between X2goSession options and paramiko.SSHCLient.connect() options 151 # if <hostname>:<port> is used for sshproxy_host, then this <port> is used 152 if sshproxy_host: 153 if sshproxy_host.find(':'): 154 self.hostname = sshproxy_host.split(':')[0] 155 try: self.port = int(sshproxy_host.split(':')[1]) 156 except IndexError: pass 157 else: 158 self.hostname = sshproxy_host 159 160 if sshproxy_user: self.username = sshproxy_user 161 if sshproxy_password: password = sshproxy_password 162 if sshproxy_force_password_auth: force_password_auth = sshproxy_force_password_auth 163 if sshproxy_key_filename: key_filename = sshproxy_key_filename 164 if sshproxy_pkey: pkey = sshproxy_pkey 165 if sshproxy_look_for_keys: look_for_keys = sshproxy_look_for_keys 166 if sshproxy_allow_agent: allow_agent = sshproxy_allow_agent 167 if sshproxy_tunnel: 168 self.local_host, self.local_port, self.remote_host, self.remote_port = sshproxy_tunnel.split(':') 169 self.local_port = int(self.local_port) 170 self.remote_port = int(self.remote_port) 171 else: 172 self.local_host = local_host 173 self.local_port = int(local_port) 174 self.remote_host = remote_host 175 self.remote_port = int(remote_port) 176 177 # allow more trailing whitespace tolerance in hostnames 178 self.hostname = self.hostname.strip() 179 self.local_host = self.local_host.strip() 180 self.remote_host = self.remote_host.strip() 181 182 # enforce IPv4 for localhost addresses!!! 183 _hostname = self.hostname 184 if _hostname in ('localhost', 'localhost.localdomain'): 185 _hostname = '127.0.0.1' 186 if self.local_host in ('localhost', 'localhost.localdomain'): 187 self.local_host = '127.0.0.1' 188 if self.remote_host in ('localhost', 'localhost.localdomain'): 189 self.remote_host = '127.0.0.1' 190 191 if username is None: 192 username = _CURRENT_LOCAL_USER 193 194 self._keepalive = True 195 self.session_instance = session_instance 196 197 self.client_instance = None 198 if self.session_instance is not None: 199 self.client_instance = self.session_instance.get_client_instance() 200 201 self.ssh_rootdir = ssh_rootdir 202 paramiko.SSHClient.__init__(self) 203 204 self.known_hosts = known_hosts 205 if self.known_hosts: 206 utils.touch_file(self.known_hosts) 207 self.load_host_keys(self.known_hosts) 208 209 if not add_to_known_hosts and session_instance: 210 self.set_missing_host_key_policy(checkhosts.X2goInteractiveAddPolicy(caller=self, session_instance=session_instance)) 211 212 if add_to_known_hosts: 213 self.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 214 215 try: 216 if key_filename or pkey or look_for_keys or allow_agent or (password and force_password_auth): 217 try: 218 if password and force_password_auth: 219 self.connect(_hostname, port=self.port, 220 username=self.username, 221 password=password, 222 key_filename=None, 223 pkey=None, 224 look_for_keys=False, 225 allow_agent=False, 226 ) 227 elif (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey: 228 self.connect(_hostname, port=self.port, 229 username=self.username, 230 key_filename=key_filename, 231 pkey=pkey, 232 look_for_keys=look_for_keys, 233 allow_agent=allow_agent, 234 ) 235 else: 236 self.connect(_hostname, port=self.port, 237 username=self.username, 238 look_for_keys=look_for_keys, 239 allow_agent=allow_agent, 240 ) 241 242 except x2go_exceptions.AuthenticationException, e: 243 self.close() 244 raise x2go_exceptions.X2goSSHProxyAuthenticationException('pubkey auth mechanisms both failed') 245 except x2go_exceptions.SSHException, e: 246 self.close() 247 raise x2go_exceptions.X2goSSHProxyAuthenticationException('interactive authentication required') 248 except: 249 raise 250 251 # since Paramiko 1.7.7.1 there is compression available, let's use it if present... 252 t = self.get_transport() 253 if x2go._paramiko.PARAMIKO_FEATURE['use-compression']: 254 t.use_compression(compress=True) 255 256 # if there is no private key, we will use the given password, if any 257 else: 258 # create a random password if password is empty to trigger host key validity check 259 if not password: 260 password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)]) 261 try: 262 self.connect(_hostname, port=self.port, 263 username=self.username, 264 password=password, 265 look_for_keys=False, 266 allow_agent=False, 267 ) 268 except x2go_exceptions.AuthenticationException: 269 self.close() 270 raise x2go_exceptions.X2goSSHProxyAuthenticationException('interactive auth mechanisms failed') 271 except: 272 self.close() 273 raise 274 275 except (x2go_exceptions.SSHException, IOError), e: 276 self.close() 277 raise x2go_exceptions.X2goSSHProxyException(str(e)) 278 except: 279 self.close() 280 raise 281 282 self.set_missing_host_key_policy(paramiko.RejectPolicy()) 283 threading.Thread.__init__(self) 284 self.daemon = True
285
286 - def check_host(self):
287 """\ 288 Wraps around a Paramiko/SSH host key check. 289 290 """ 291 # hostname rewrite for localhost, force to IPv4 292 _hostname = self.hostname 293 294 # force into IPv4 for localhost connections 295 if _hostname in ('localhost', 'localhost.localdomain'): 296 _hostname = '127.0.0.1' 297 298 _valid = False 299 (_valid, _hostname, _port, _fingerprint, _fingerprint_type) = checkhosts.check_ssh_host_key(self, _hostname, port=self.port) 300 if not _valid and self.session_instance: 301 _valid = self.session_instance.HOOK_check_host_dialog(_hostname, _port, fingerprint=_fingerprint, fingerprint_type=_fingerprint_type) 302 return _valid
303
304 - def run(self):
305 """\ 306 Start the SSH proxying tunnel... 307 308 @raise X2goSSHProxyException: if the SSH proxy could not retrieve an SSH transport for proxying a X2Go server-client connection 309 310 """ 311 if self.get_transport() is not None and self.get_transport().is_authenticated(): 312 self.local_port = utils.detect_unused_port(bind_address=self.local_host, preferred_port=self.local_port) 313 if self.client_instance is not None: 314 _profile_id = self.session_instance.get_profile_id() 315 if self.client_instance.session_profiles.has_profile(_profile_id): 316 self.client_instance.session_profiles.update_value(_profile_id, 317 'sshproxytunnel', 318 '%s:%s:%s:%s' % (self.local_host, self.local_port, self.remote_host, self.remote_port) 319 ) 320 self.client_instance.session_profiles.write_user_config = True 321 self.client_instance.session_profiles.write() 322 self.fw_tunnel = forward.start_forward_tunnel(local_host=self.local_host, 323 local_port=self.local_port, 324 remote_host=self.remote_host, 325 remote_port=self.remote_port, 326 ssh_transport=self.get_transport(), 327 logger=self.logger, ) 328 self.logger('SSH proxy tunnel via [%s]:%s has been set up' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE) 329 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) 330 331 while self._keepalive: 332 gevent.sleep(.1) 333 334 else: 335 raise x2go_exceptions.X2goSSHProxyException('SSH proxy connection could not retrieve an SSH transport')
336
337 - def get_local_proxy_port(self):
338 """\ 339 Retrieve the local IP socket port this SSH proxying tunnel is (about to) bind/bound to. 340 341 @return: local IP socket port 342 @rtype: C{int} 343 344 """ 345 return self.local_port
346
347 - def stop_thread(self):
348 """\ 349 Tear down the SSH proxying tunnel. 350 351 """ 352 if self.fw_tunnel is not None and self.fw_tunnel.is_active: 353 self.logger('taking down SSH proxy tunnel via [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE) 354 try: forward.stop_forward_tunnel(self.fw_tunnel) 355 except: pass 356 self.fw_tunnel = None 357 self._keepalive = False 358 if self.get_transport() is not None: 359 self.logger('closing SSH proxy connection to [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE) 360 self.close() 361 self.password = self.sshproxy_password = None
362
363 - def __del__(self):
364 """\ 365 Class desctructor. 366 367 """ 368 self.stop_thread()
369