diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java index 90214e519..09ab19163 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/BasicTunnelRequestUtility.java @@ -42,6 +42,7 @@ import org.glyptodon.guacamole.net.event.TunnelCloseEvent; import org.glyptodon.guacamole.net.event.TunnelConnectEvent; import org.glyptodon.guacamole.net.event.listener.TunnelCloseListener; import org.glyptodon.guacamole.net.event.listener.TunnelConnectListener; +import org.glyptodon.guacamole.properties.GuacamoleProperties; import org.glyptodon.guacamole.protocol.GuacamoleClientInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -329,8 +330,19 @@ public class BasicTunnelRequestUtility { @Override public GuacamoleReader acquireReader() { - // Monitor instructions which pertain to server-side events - return new MonitoringGuacamoleReader(clipboard, super.acquireReader()); + + // Monitor instructions which pertain to server-side events, if necessary + try { + if (GuacamoleProperties.getProperty(CaptureClipboard.INTEGRATION_ENABLED, false)) + return new MonitoringGuacamoleReader(clipboard, super.acquireReader()); + } + catch (GuacamoleException e) { + logger.warn("Clipboard integration disabled due to error.", e); + } + + // Pass through by default. + return super.acquireReader(); + } @Override diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/CaptureClipboard.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/CaptureClipboard.java index f28309808..65a5a2ba8 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/CaptureClipboard.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/CaptureClipboard.java @@ -71,8 +71,11 @@ public class CaptureClipboard extends AuthenticatingHttpServlet { // Send clipboard contents try { - response.setContentType("text/plain"); - response.getWriter().print(clipboard.waitForContents(CLIPBOARD_TIMEOUT)); + synchronized (clipboard) { + clipboard.waitForContents(CLIPBOARD_TIMEOUT); + response.setContentType(clipboard.getMimetype()); + response.getOutputStream().write(clipboard.getContents()); + } } catch (IOException e) { throw new GuacamoleServerException("Unable to send clipboard contents", e); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/ClipboardState.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/ClipboardState.java index b656e7de5..8a1fca151 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/ClipboardState.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/ClipboardState.java @@ -31,11 +31,36 @@ package org.glyptodon.guacamole.net.basic; */ public class ClipboardState { + /** + * The maximum number of bytes to track. + */ + private static final int MAXIMUM_LENGTH = 262144; + + /** + * The mimetype of the current contents. + */ + private String mimetype = "text/plain"; + + /** + * The mimetype of the pending contents. + */ + private String pending_mimetype = "text/plain"; + /** * The current contents. */ - private String contents = ""; + private byte[] contents = new byte[0]; + /** + * The pending clipboard contents. + */ + private final byte[] pending = new byte[MAXIMUM_LENGTH]; + + /** + * The length of the pending data, in bytes. + */ + private int pending_length = 0; + /** * The timestamp of the last contents update. */ @@ -45,38 +70,84 @@ public class ClipboardState { * Returns the current clipboard contents. * @return The current clipboard contents */ - public synchronized String getContents() { + public synchronized byte[] getContents() { return contents; } /** - * Sets the current clipboard contents. - * @param contents The contents to assign to the clipboard. + * Returns the mimetype of the current clipboard contents. + * @return The mimetype of the current clipboard contents. */ - public synchronized void setContents(String contents) { - this.contents = contents; - last_update = System.currentTimeMillis(); - this.notifyAll(); + public synchronized String getMimetype() { + return mimetype; } /** - * Wait up to the given timeout for new clipboard data. If data more recent - * than the timeout period is available, return that. + * Begins a new update of the clipboard contents. The actual contents will + * not be saved until commit() is called. + * + * @param mimetype The mimetype of the contents being added. + */ + public synchronized void begin(String mimetype) { + pending_length = 0; + this.pending_mimetype = mimetype; + } + + /** + * Appends the given data to the clipboard contents. + * + * @param data The raw data to append. + */ + public synchronized void append(byte[] data) { + + // Calculate size of copy + int length = data.length; + int remaining = pending.length - pending_length; + if (remaining < length) + length = remaining; + + // Append data + System.arraycopy(data, 0, pending, pending_length, length); + pending_length += length; + + } + + /** + * Commits the pending contents to the clipboard, notifying any threads + * waiting for clipboard updates. + */ + public synchronized void commit() { + + // Commit contents + mimetype = pending_mimetype; + contents = new byte[pending_length]; + System.arraycopy(pending, 0, contents, 0, pending_length); + + // Notify of update + last_update = System.currentTimeMillis(); + this.notifyAll(); + + } + + /** + * Wait up to the given timeout for new clipboard data. * * @param timeout The amount of time to wait, in milliseconds. - * @return The current clipboard contents. + * @return true if the contents were updated within the timeframe given, + * false otherwise. */ - public synchronized String waitForContents(int timeout) { + public synchronized boolean waitForContents(int timeout) { // Wait for new contents if it's been a while if (System.currentTimeMillis() - last_update > timeout) { try { this.wait(timeout); + return true; } catch (InterruptedException e) { /* ignore */ } } - return getContents(); + return false; } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/MonitoringGuacamoleReader.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/MonitoringGuacamoleReader.java index 9c3b4bdfa..73bbc9fe3 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/MonitoringGuacamoleReader.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/MonitoringGuacamoleReader.java @@ -23,6 +23,7 @@ package org.glyptodon.guacamole.net.basic; import java.util.List; +import javax.xml.bind.DatatypeConverter; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.io.GuacamoleReader; import org.glyptodon.guacamole.protocol.GuacamoleInstruction; @@ -45,6 +46,11 @@ public class MonitoringGuacamoleReader implements GuacamoleReader { */ private final ClipboardState clipboard; + /** + * The index of the clipboard stream, if any. + */ + private String clipboard_stream_index = null; + /** * Creates a new MonitoringGuacamoleReader which watches the instructions * read by the given GuacamoleReader, firing events when specific @@ -84,11 +90,31 @@ public class MonitoringGuacamoleReader implements GuacamoleReader { if (instruction == null) return null; - // If clipboard changed, notify listeners + // If clipboard changing, reset clipboard state if (instruction.getOpcode().equals("clipboard")) { List args = instruction.getArgs(); - if (args.size() >= 1) - clipboard.setContents(args.get(0)); + if (args.size() >= 2) { + clipboard_stream_index = args.get(0); + clipboard.begin(args.get(1)); + } + } + + // Add clipboard blobs to existing streams + else if (instruction.getOpcode().equals("blob")) { + List args = instruction.getArgs(); + if (args.size() >= 2 && args.get(0).equals(clipboard_stream_index)) { + String base64 = args.get(1); + clipboard.append(DatatypeConverter.parseBase64Binary(base64)); + } + } + + // Terminate and update clipboard at end of stream + else if (instruction.getOpcode().equals("end")) { + List args = instruction.getArgs(); + if (args.size() >= 1 && args.get(0).equals(clipboard_stream_index)) { + clipboard.commit(); + clipboard_stream_index = null; + } } return instruction;