-
Notifications
You must be signed in to change notification settings - Fork 696
/
GuacamoleWebSocketTunnelEndpoint.java
373 lines (309 loc) · 13 KB
/
GuacamoleWebSocketTunnelEndpoint.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.websocket;
import java.io.IOException;
import java.util.List;
import javax.websocket.CloseReason;
import javax.websocket.CloseReason.CloseCode;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.io.GuacamoleReader;
import org.apache.guacamole.io.GuacamoleWriter;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleConnectionClosedException;
import org.apache.guacamole.protocol.FilteredGuacamoleWriter;
import org.apache.guacamole.protocol.GuacamoleFilter;
import org.apache.guacamole.protocol.GuacamoleInstruction;
import org.apache.guacamole.protocol.GuacamoleStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A WebSocket implementation of GuacamoleTunnel functionality, compatible with
* the Guacamole.WebSocketTunnel object included with the JavaScript API.
* Messages sent/received are simply chunks of the Guacamole protocol
* instruction stream.
*/
public abstract class GuacamoleWebSocketTunnelEndpoint extends Endpoint {
/**
* The default, minimum buffer size for instructions.
*/
private static final int BUFFER_SIZE = 8192;
/**
* The opcode of the instruction used to indicate a connection stability
* test ping request or response. Note that this instruction is
* encapsulated within an internal tunnel instruction (with the opcode
* being the empty string), thus this will actually be the value of the
* first element of the received instruction.
*/
private static final String PING_OPCODE = "ping";
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelEndpoint.class);
/**
* The underlying GuacamoleTunnel. WebSocket reads/writes will be handled
* as reads/writes to this tunnel. This value may be null if no connection
* has been established.
*/
private GuacamoleTunnel tunnel;
/**
* Remote (client) side of this connection. This value will always be
* non-null if tunnel is non-null.
*/
private RemoteEndpoint.Basic remote;
/**
* Sends the numeric Guacaomle Status Code and Web Socket
* code and closes the connection.
*
* @param session
* The outbound WebSocket connection to close.
*
* @param guacamoleStatusCode
* The numeric Guacamole status to send.
*
* @param webSocketCode
* The numeric WebSocket status to send.
*/
private void closeConnection(Session session, int guacamoleStatusCode,
int webSocketCode) {
try {
CloseCode code = CloseReason.CloseCodes.getCloseCode(webSocketCode);
String message = Integer.toString(guacamoleStatusCode);
session.close(new CloseReason(code, message));
}
catch (IOException e) {
logger.debug("Unable to close WebSocket connection.", e);
}
}
/**
* Sends the given Guacaomle Status and closes the given
* connection.
*
* @param session
* The outbound WebSocket connection to close.
*
* @param guacStatus
* The status to use for the connection.
*/
private void closeConnection(Session session, GuacamoleStatus guacStatus) {
closeConnection(session, guacStatus.getGuacamoleStatusCode(),
guacStatus.getWebSocketCode());
}
/**
* Sends a Guacamole instruction along the outbound WebSocket connection to
* the connected Guacamole client. If an instruction is already in the
* process of being sent by another thread, this function will block until
* in-progress instructions are complete.
*
* @param instruction
* The instruction to send.
*
* @throws IOException
* If an I/O error occurs preventing the given instruction from being
* sent.
*/
private void sendInstruction(String instruction)
throws IOException {
// NOTE: Synchronization on the non-final remote field here is
// intentional. The remote (the outbound websocket connection) is only
// sensitive to simultaneous attempts to send messages with respect to
// itself. If the remote changes, then the outbound websocket
// connection has changed, and synchronization need only be performed
// in context of the new remote.
synchronized (remote) {
remote.sendText(instruction);
}
}
/**
* Sends a Guacamole instruction along the outbound WebSocket connection to
* the connected Guacamole client. If an instruction is already in the
* process of being sent by another thread, this function will block until
* in-progress instructions are complete.
*
* @param instruction
* The instruction to send.
*
* @throws IOException
* If an I/O error occurs preventing the given instruction from being
* sent.
*/
private void sendInstruction(GuacamoleInstruction instruction)
throws IOException {
sendInstruction(instruction.toString());
}
/**
* Returns a new tunnel for the given session. How this tunnel is created
* or retrieved is implementation-dependent.
*
* @param session The session associated with the active WebSocket
* connection.
* @param config Configuration information associated with the instance of
* the endpoint created for handling this single connection.
* @return A connected tunnel, or null if no such tunnel exists.
* @throws GuacamoleException If an error occurs while retrieving the
* tunnel, or if access to the tunnel is denied.
*/
protected abstract GuacamoleTunnel createTunnel(Session session, EndpointConfig config)
throws GuacamoleException;
@Override
@OnOpen
public void onOpen(final Session session, EndpointConfig config) {
// Store underlying remote for future use via sendInstruction()
remote = session.getBasicRemote();
try {
// Get tunnel
tunnel = createTunnel(session, config);
if (tunnel == null) {
closeConnection(session, GuacamoleStatus.RESOURCE_NOT_FOUND);
return;
}
}
catch (GuacamoleException e) {
logger.error("Creation of WebSocket tunnel to guacd failed: {}", e.getMessage());
logger.debug("Error connecting WebSocket tunnel.", e);
closeConnection(session, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
return;
}
// Manually register message handler
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String message) {
GuacamoleWebSocketTunnelEndpoint.this.onMessage(message);
}
});
// Prepare read transfer thread
Thread readThread = new Thread() {
@Override
public void run() {
StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
GuacamoleReader reader = tunnel.acquireReader();
char[] readMessage;
try {
// Send tunnel UUID
sendInstruction(new GuacamoleInstruction(
GuacamoleTunnel.INTERNAL_DATA_OPCODE,
tunnel.getUUID().toString()
));
try {
// Attempt to read
while ((readMessage = reader.read()) != null) {
// Buffer message
buffer.append(readMessage);
// Flush if we expect to wait or buffer is getting full
if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
sendInstruction(buffer.toString());
buffer.setLength(0);
}
}
// No more data
closeConnection(session, GuacamoleStatus.SUCCESS);
}
// Catch any thrown guacamole exception and attempt
// to pass within the WebSocket connection, logging
// each error appropriately.
catch (GuacamoleClientException e) {
logger.info("WebSocket connection terminated: {}", e.getMessage());
logger.debug("WebSocket connection terminated due to client error.", e);
closeConnection(session, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
catch (GuacamoleConnectionClosedException e) {
logger.debug("Connection to guacd closed.", e);
closeConnection(session, GuacamoleStatus.SUCCESS);
}
catch (GuacamoleException e) {
logger.error("Connection to guacd terminated abnormally: {}", e.getMessage());
logger.debug("Internal error during connection to guacd.", e);
closeConnection(session, e.getStatus().getGuacamoleStatusCode(),
e.getWebSocketCode());
}
}
catch (IOException e) {
logger.debug("I/O error prevents further reads.", e);
closeConnection(session, GuacamoleStatus.SERVER_ERROR);
}
}
};
readThread.start();
}
@OnMessage
public void onMessage(String message) {
// Ignore inbound messages if there is no associated tunnel
if (tunnel == null)
return;
// Filter received instructions, handling tunnel-internal instructions
// without passing through to guacd
GuacamoleWriter writer = new FilteredGuacamoleWriter(tunnel.acquireWriter(), new GuacamoleFilter() {
@Override
public GuacamoleInstruction filter(GuacamoleInstruction instruction)
throws GuacamoleException {
// Filter out all tunnel-internal instructions
if (instruction.getOpcode().equals(GuacamoleTunnel.INTERNAL_DATA_OPCODE)) {
// Respond to ping requests
List<String> args = instruction.getArgs();
if (args.size() >= 2 && args.get(0).equals(PING_OPCODE)) {
try {
sendInstruction(new GuacamoleInstruction(
GuacamoleTunnel.INTERNAL_DATA_OPCODE,
PING_OPCODE, args.get(1)
));
}
catch (IOException e) {
logger.debug("Unable to send \"ping\" response for WebSocket tunnel.", e);
}
}
return null;
}
// Pass through all non-internal instructions untouched
return instruction;
}
});
try {
// Write received message
writer.write(message.toCharArray());
}
catch (GuacamoleConnectionClosedException e) {
logger.debug("Connection to guacd closed.", e);
}
catch (GuacamoleException e) {
logger.debug("WebSocket tunnel write failed.", e);
}
tunnel.releaseWriter();
}
@Override
@OnClose
public void onClose(Session session, CloseReason closeReason) {
try {
if (tunnel != null)
tunnel.close();
}
catch (GuacamoleException e) {
logger.debug("Unable to close WebSocket tunnel.", e);
}
}
}