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 from monkey_patch_paramiko import monkey_patch_paramiko
48 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, 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
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
159 self.hostname = self.hostname.strip()
160 self.local_host = self.local_host.strip()
161 self.remote_host = self.remote_host.strip()
162
163
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
225 t = self.get_transport()
226 if hasattr(t, 'use_compression'):
227 t.use_compression(compress=True)
228
229
230 else:
231
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
260 """\
261 Wraps around a Paramiko/SSH host key check.
262
263 """
264
265 _hostname = self.hostname
266
267
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
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
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
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
337 """\
338 Class desctructor.
339
340 """
341 self.stop_thread()
342