1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """\
21 Python Gevent based port forwarding server (openssh -L option) for the
22 proxying of graphical X2Go elements.
23
24 """
25 __NAME__ = "x2gofwtunnel-pylib"
26
27
28 import copy
29
30
31 import gevent
32 from gevent import select, socket
33 from gevent.server import StreamServer
34
35
36 import log
37 from defaults import X2GOCLIENT_OS as _X2GOCLIENT_OS
38
40 """\
41 L{X2goFwServer} implements a gevent's StreamServer based Paramiko/SSH port
42 forwarding server.
43
44 An L{X2goFwServer} class object is used to tunnel graphical trafic
45 through an external proxy command launched by a C{X2goProxy*} backend.
46
47 """
48 - def __init__ (self, listener, remote_host, remote_port, ssh_transport, session_instance=None, logger=None, loglevel=log.loglevel_DEFAULT,):
49 """\
50 @param listener: listen on TCP/IP socket C{(<IP>, <Port>)}
51 @type listener: C{tuple}
52 @param remote_host: hostname or IP of remote host (in case of X2Go mostly 127.0.0.1)
53 @type remote_host: C{str}
54 @param remote_port: port of remote host
55 @type remote_port: C{int}
56 @param ssh_transport: a valid Paramiko/SSH transport object
57 @type ssh_transport: C{obj}
58 @param logger: you can pass an L{X2goLogger} object to the
59 L{X2goFwServer} constructor
60 @type logger: C{obj}
61 @param loglevel: if no L{X2goLogger} object has been supplied a new one will be
62 constructed with the given loglevel
63 @type loglevel: C{int}
64
65 """
66 if logger is None:
67 self.logger = log.X2goLogger(loglevel=loglevel)
68 else:
69 self.logger = copy.deepcopy(logger)
70 self.logger.tag = __NAME__
71
72 self.chan = None
73 self.is_active = False
74 self.failed = False
75 self.keepalive = False
76 self.chain_host = remote_host
77 self.chain_port = remote_port
78 self.ssh_transport = ssh_transport
79 self.session_instance = session_instance
80
81 self.fw_socket = None
82
83 StreamServer.__init__(self, listener, self.x2go_forward_tunnel_handle)
84
86 """\
87 Handle for SSH/Paramiko forwarding tunnel.
88
89 @param fw_socket: local end of the forwarding tunnel
90 @type fw_socket: C{obj}
91 @param address: unused/ignored
92 @type address: C{tuple}
93
94 """
95 self.fw_socket = fw_socket
96
97
98 self.fw_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
99
100 _success = False
101 _count = 0
102 _maxwait = 20
103
104 while not _success and _count < _maxwait:
105
106
107 if self.session_instance:
108 if not self.session_instance.is_connected():
109 break
110
111 _count += 1
112 try:
113 self.chan = self.ssh_transport.open_channel('direct-tcpip',
114 (self.chain_host, self.chain_port),
115 self.fw_socket.getpeername())
116 chan_peername = self.chan.getpeername()
117 _success = True
118 except Exception, e:
119 self.logger('incoming request to %s:%d failed on attempt %d of %d: %s' % (self.chain_host,
120 self.chain_port,
121 _count,
122 _maxwait,
123 repr(e)),
124 loglevel=log.loglevel_WARN)
125 gevent.sleep(.4)
126
127 if not _success:
128 self.logger('incoming request to %s:%d failed after %d attempts' % (self.chain_host,
129 self.chain_port,
130 _count),
131 loglevel=log.loglevel_ERROR)
132 if self.session_instance:
133 self.session_instance.HOOK_forwarding_tunnel_setup_failed(chain_host=self.chain_host, chain_port=self.chain_port)
134 self.failed = True
135
136 else:
137
138 self.logger('connected! Tunnel open %r -> %r -> %r' % (self.fw_socket.getpeername(),
139 chan_peername, (self.chain_host, self.chain_port)),
140 loglevel=log.loglevel_INFO)
141
142
143 self.is_active = True
144
145 self.keepalive = True
146 try:
147 while self.keepalive:
148 r, w, x = select.select([self.fw_socket, self.chan], [], [])
149 if fw_socket in r:
150 data = fw_socket.recv(1024)
151 if len(data) == 0:
152 break
153 self.chan.send(data)
154 if self.chan in r:
155 data = self.chan.recv(1024)
156 if len(data) == 0:
157 break
158 fw_socket.send(data)
159 self.close_channel()
160 self.close_socket()
161 except socket.error:
162 pass
163
164 self.is_active = False
165 self.logger('Tunnel closed from %r' % (chan_peername,),
166 loglevel=log.loglevel_INFO)
167
169 """\
170 Close an open channel again.
171
172 """
173
174 if self.chan is not None:
175 try:
176 if _X2GOCLIENT_OS != 'Windows':
177 self.chan.close()
178 self.chan = None
179 except EOFError:
180 pass
181
183 """\
184 Close the forwarding tunnel's socket again.
185
186 """
187 _success = False
188 _count = 0
189 _maxwait = 20
190
191
192 while not _success and _count < _maxwait:
193 _count += 1
194 try:
195 self.close_channel()
196 if self.fw_socket is not None:
197 self.fw_socket.close()
198 _success = True
199 except socket.error:
200 gevent.sleep(.2)
201 self.logger('could not close fw_tunnel socket, try again (%s of %s)' % (_count, _maxwait), loglevel=log.loglevel_WARN)
202
203 if _count >= _maxwait:
204 self.logger('forwarding tunnel to [%s]:%d could not be closed properly' % (self.chain_host, self.chain_port), loglevel=log.loglevel_WARN)
205
207 """\
208 Stop the forwarding tunnel.
209
210 """
211 self.close_socket()
212 StreamServer.stop(self)
213
214
215 -def start_forward_tunnel(local_host='127.0.0.1', local_port=22022,
216 remote_host='127.0.0.1', remote_port=22,
217 ssh_transport=None,
218 session_instance=None,
219 logger=None, ):
220 """\
221 Setup up a Paramiko/SSH port forwarding tunnel (like openssh -L option).
222
223 The tunnel is used to transport X2Go graphics data through a proxy application like nxproxy.
224
225 @param local_host: local starting point of the forwarding tunnel
226 @type local_host: C{int}
227 @param local_port: listen port of the local starting point
228 @type local_port: C{int}
229 @param remote_host: from the endpoint of the tunnel, connect to host C{<remote_host>}...
230 @type remote_host: C{str}
231 @param remote_port: ... on port C{<remote_port>}
232 @type remote_port: C{int}
233 @param ssh_transport: the Paramiko/SSH transport (i.e. the X2Go sessions Paramiko/SSH transport object)
234 @type ssh_transport: C{obj}
235 @param session_instance: the L{X2goSession} instance that initiates this tunnel
236 @type session_instance: C{obj}
237 @param logger: an X2goLogger object
238 @type logger: C{obj}
239
240 @return: returns an L{X2goFwServer} instance
241 @rtype: C{obj}
242
243 """
244 fw_server = X2goFwServer(listener=(local_host, local_port),
245 remote_host=remote_host, remote_port=remote_port,
246 ssh_transport=ssh_transport, session_instance=session_instance,
247 logger=logger,
248 )
249 try:
250 fw_server.start()
251 except socket.error:
252 fw_server.failed = True
253 fw_server.is_active = False
254 pass
255
256 return fw_server
257
259 """\
260 Tear down a given Paramiko/SSH port forwarding tunnel.
261
262 @param fw_server: an L{X2goFwServer} instance as returned by the L{start_forward_tunnel()} function
263 @type fw_server: C{obj}
264
265 """
266 if fw_server is not None:
267 fw_server.keepalive = False
268 gevent.sleep(.5)
269 fw_server.stop()
270
271
272 if __name__ == '__main__':
273 pass
274