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 import socket
33
34 import string
35 import random
36
37
38 import x2go.forward as forward
39 import x2go.checkhosts as checkhosts
40 import x2go.log as log
41 import x2go.utils as utils
42 from x2go.x2go_exceptions import *
43
44 from x2go.defaults import CURRENT_LOCAL_USER as _CURRENT_LOCAL_USER
45 from x2go.defaults import LOCAL_HOME as _LOCAL_HOME
46 from x2go.defaults import X2GO_SSH_ROOTDIR as _X2GO_SSH_ROOTDIR
47
48 from monkey_patch_paramiko import monkey_patch_paramiko
49 monkey_patch_paramiko()
50
52 """\
53 X2goSSHProxy can be used to proxy X2go connections through a firewall via SSH.
54
55 """
56 fw_tunnel = None
57
58 - def __init__(self, hostname=None, port=22, username=None, password=None, key_filename=None,
59 local_host='localhost', local_port=22022, remote_host='localhost', remote_port=22,
60 known_hosts=None, add_to_known_hosts=False, pkey=None,
61 sshproxy_host=None, sshproxy_port=22, sshproxy_user=None,
62 sshproxy_password=None, sshproxy_key_filename=None, sshproxy_pkey=None,
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
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 """
125 if logger is None:
126 self.logger = log.X2goLogger(loglevel=loglevel)
127 else:
128 self.logger = copy.deepcopy(logger)
129 self.logger.tag = __NAME__
130
131 self.hostname, self.port, self.username = hostname, port, username
132
133
134 if sshproxy_host:
135 if sshproxy_host.find(':'):
136 self.hostname = sshproxy_host.split(':')[0]
137 try: self.port = int(sshproxy_host.split(':')[1])
138 except IndexError: pass
139 else:
140 self.hostname = sshproxy_host
141
142 if sshproxy_user: self.username = sshproxy_user
143 if sshproxy_password: password = sshproxy_password
144 if sshproxy_key_filename: key_filename = sshproxy_key_filename
145 if sshproxy_pkey: pkey = sshproxy_pkey
146 if sshproxy_tunnel:
147 self.local_host, self.local_port, self.remote_host, self.remote_port = sshproxy_tunnel.split(':')
148 self.local_port = int(self.local_port)
149 self.remote_port = int(self.remote_port)
150 else:
151 self.local_host = local_host
152 self.local_port = int(local_port)
153 self.remote_host = remote_host
154 self.remote_port = int(remote_port)
155
156
157 self.hostname = self.hostname.strip()
158 self.local_host = self.local_host.strip()
159 self.remote_host = self.remote_host.strip()
160
161
162 _hostname = self.hostname
163 if _hostname in ('localhost', 'localhost.localdomain'):
164 _hostname = '127.0.0.1'
165 if self.local_host in ('localhost', 'localhost.localdomain'):
166 self.local_host = '127.0.0.1'
167 if self.remote_host in ('localhost', 'localhost.localdomain'):
168 self.remote_host = '127.0.0.1'
169
170 if username is None:
171 username = _CURRENT_LOCAL_USER
172
173 self._keepalive = True
174
175 self.ssh_rootdir = ssh_rootdir
176 paramiko.SSHClient.__init__(self)
177
178 self.known_hosts = known_hosts
179 if self.known_hosts:
180 utils.touch_file(self.known_hosts)
181 self.load_host_keys(self.known_hosts)
182
183 if not add_to_known_hosts and session_instance:
184 self.set_missing_host_key_policy(checkhosts.X2goInteractiveAddPolicy(caller=self, session_instance=session_instance))
185
186 if add_to_known_hosts:
187 self.set_missing_host_key_policy(paramiko.AutoAddPolicy())
188
189 try:
190 if (key_filename and os.path.exists(os.path.normpath(key_filename))) or pkey:
191 try:
192 self.connect(_hostname, port=self.port,
193 username=self.username,
194 key_filename=key_filename,
195 pkey=pkey,
196 look_for_keys=False,
197 allow_agent=False,
198 )
199 except AuthenticationException, e:
200 self.close()
201 raise X2goSSHProxyAuthenticationException('pubkey auth mechanisms both failed')
202 except:
203 self.close()
204 raise
205
206
207 else:
208
209 if not password:
210 password = "".join([random.choice(string.letters+string.digits) for x in range(1, 20)])
211 try:
212 self.connect(_hostname, port=self.port,
213 username=self.username,
214 password=password,
215 look_for_keys=False,
216 allow_agent=False,
217 )
218 except AuthenticationException:
219 self.close()
220 raise X2goSSHProxyAuthenticationException('interactive auth mechanisms failed')
221 except:
222 self.close()
223 raise
224
225 except paramiko.SSHException, e:
226 self.close()
227 raise X2goSSHProxyException(str(e))
228 except:
229 self.close()
230 raise
231
232 self.set_missing_host_key_policy(paramiko.RejectPolicy())
233 threading.Thread.__init__(self)
234 self.daemon = True
235
237 """\
238 Wraps around a Paramiko/SSH host key check.
239
240 """
241
242
243 _hostname = self.hostname
244
245
246 if _hostname in ('localhost', 'localhost.localdomain'):
247 _hostname = '127.0.0.1'
248
249 _valid = False
250 (_valid, _hostname, _port, _fingerprint, _fingerprint_type) = checkhosts.check_ssh_host_key(self, _hostname, port=self.port)
251 if not _valid and self.session_instance:
252 _valid = self.session_instance.HOOK_check_host_dialog(_hostname, _port, fingerprint=_fingerprint, fingerprint_type=_fingerprint_type)
253 return _valid
254
256 """\
257 Start the SSH proxying tunnel...
258
259 """
260 if self.get_transport() is not None and self.get_transport().is_authenticated():
261 self.local_port = utils.detect_unused_port(bind_address=self.local_host, preferred_port=self.local_port)
262 self.fw_tunnel = forward.start_forward_tunnel(local_host=self.local_host,
263 local_port=self.local_port,
264 remote_host=self.remote_host,
265 remote_port=self.remote_port,
266 ssh_transport=self.get_transport(),
267 logger=self.logger, )
268 self.logger('SSH proxy tunnel via [%s]:%s has been set up' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
269 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)
270
271 while self._keepalive:
272 gevent.sleep(.1)
273
274 else:
275 raise X2goSSHProxyException('SSH proxy connection could not retrieve an SSH transport')
276
278 """\
279 Retrieve the local IP socket port this SSH proxying tunnel is (about to) bind/bound to.
280
281 @return: local IP socket port
282 @rtype: C{int}
283 """
284 return self.local_port
285
287 """\
288 Tear down the SSH proxying tunnel.
289
290 """
291 if self.fw_tunnel is not None and self.fw_tunnel.is_active:
292 self.logger('taking down SSH proxy tunnel via [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
293 try: forward.stop_forward_tunnel(self.fw_tunnel)
294 except: pass
295 self.fw_tunnel = None
296 self._keepalive = False
297 if self.get_transport() is not None:
298 self.logger('closing SSH proxy connection to [%s]:%s' % (self.hostname, self.port), loglevel=log.loglevel_NOTICE)
299 self.close()
300 self.password = self.sshproxy_password = None
301
303 """\
304 Class desctructor.
305
306 """
307 self.stop_thread()
308