1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """\
21 L{X2GoSSHProxy} class - providing a forwarding tunnel for connecting to servers behind firewalls.
22
23 """
24 __NAME__ = 'x2gosshproxy-pylib'
25
26
27 import gevent
28 import os
29 import copy
30 import paramiko
31 import threading
32
33 import string
34 import random
35
36
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
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
151
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
178 self.hostname = self.hostname.strip()
179 self.local_host = self.local_host.strip()
180 self.remote_host = self.remote_host.strip()
181
182
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
252 t = self.get_transport()
253 if x2go._paramiko.PARAMIKO_FEATURE['use-compression']:
254 t.use_compression(compress=True)
255
256
257 else:
258
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
287 """\
288 Wraps around a Paramiko/SSH host key check.
289
290 """
291 _hostname = self.hostname
292
293
294 if _hostname in ('localhost', 'localhost.localdomain'):
295 _hostname = '127.0.0.1'
296
297 _valid = False
298 (_valid, _hostname, _port, _fingerprint, _fingerprint_type) = checkhosts.check_ssh_host_key(self, _hostname, port=self.port)
299 if not _valid and self.session_instance:
300 _valid = self.session_instance.HOOK_check_host_dialog(self.remote_host, self.remote_port, fingerprint=_fingerprint, fingerprint_type=_fingerprint_type)
301 return _valid
302
304 """\
305 Start the SSH proxying tunnel...
306
307 @raise X2GoSSHProxyException: if the SSH proxy could not retrieve an SSH transport for proxying a X2Go server-client connection
308
309 """
310 if self.get_transport() is not None and self.get_transport().is_authenticated():
311 self.local_port = utils.detect_unused_port(bind_address=self.local_host, preferred_port=self.local_port)
312 if self.client_instance is not None:
313 _profile_id = self.session_instance.get_profile_id()
314 if self.client_instance.session_profiles.has_profile(_profile_id):
315 self.client_instance.session_profiles.update_value(_profile_id,
316 'sshproxytunnel',
317 '%s:%s:%s:%s' % (self.local_host, self.local_port, self.remote_host, self.remote_port)
318 )
319 self.client_instance.session_profiles.write_user_config = True
320 self.client_instance.session_profiles.write()
321 self.fw_tunnel = forward.start_forward_tunnel(local_host=self.local_host,
322 local_port=self.local_port,
323 remote_host=self.remote_host,
324 remote_port=self.remote_port,
325 ssh_transport=self.get_transport(),
326 logger=self.logger, )
327 self.logger('SSH proxy tunnel via [%s]:%s has been set up' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
328 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)
329
330 while self._keepalive:
331 gevent.sleep(.1)
332
333 else:
334 raise x2go_exceptions.X2GoSSHProxyException('SSH proxy connection could not retrieve an SSH transport')
335
337 """\
338 Retrieve the local IP socket address this SSH proxying tunnel is (about to) bind/bound to.
339
340 @return: local IP socket address
341 @rtype: C{str}
342
343 """
344 return self.local_host
345
347 """\
348 Retrieve the local IP socket port this SSH proxying tunnel is (about to) bind/bound to.
349
350 @return: local IP socket port
351 @rtype: C{int}
352
353 """
354 return self.local_port
355
357 """\
358 Retrieve the remote IP socket address at the remote end of the SSH proxying tunnel.
359
360 @return: remote IP socket address
361 @rtype: C{str}
362
363 """
364 return self.remote_host
365
367 """\
368 Retrieve the remote IP socket port of the target system's SSH daemon.
369
370 @return: remote SSH port
371 @rtype: C{int}
372
373 """
374 return self.remote_port
375
377 """\
378 Tear down the SSH proxying tunnel.
379
380 """
381 if self.fw_tunnel is not None and self.fw_tunnel.is_active:
382 self.logger('taking down SSH proxy tunnel via [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
383 try: forward.stop_forward_tunnel(self.fw_tunnel)
384 except: pass
385 self.fw_tunnel = None
386 self._keepalive = False
387 if self.get_transport() is not None:
388 self.logger('closing SSH proxy connection to [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
389 self.close()
390 self.password = self.sshproxy_password = None
391
393 """\
394 Class desctructor.
395
396 """
397 self.stop_thread()
398