Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* 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.
*/

var Guacamole = Guacamole || {};

/**
* An interpreter for clipboard events within a Guacamole session recording.
* Clipboard data arrives as a sequence of instructions: clipboard (declares
* stream and mimetype), blob (contains base64 data), and end (terminates stream).
*
* @constructor
* @param {number} [startTimestamp=0]
* The starting timestamp for the recording. Event timestamps will be
* relative to this value.
*/
Guacamole.ClipboardEventInterpreter = function ClipboardEventInterpreter(startTimestamp) {

if (startTimestamp === undefined || startTimestamp === null)
startTimestamp = 0;

/**
* All clipboard events parsed so far.
*
* @private
* @type {!Guacamole.ClipboardEventInterpreter.ClipboardEvent[]}
*/
var parsedEvents = [];

/**
* Map of active clipboard streams, keyed by stream index.
* Each entry tracks the mimetype and accumulated base64 data.
*
* @private
* @type {Object.<string, {mimetype: string, data: string, timestamp: number}>}
*/
var activeStreams = {};

/**
* The timestamp of the most recent instruction, used for events
* that don't have their own timestamp.
*
* @private
* @type {number}
*/
var lastTimestamp = 0;

/**
* Updates the last known timestamp.
*
* @param {number} timestamp
* The absolute timestamp from a sync instruction or key event.
*/
this.setTimestamp = function setTimestamp(timestamp) {
lastTimestamp = timestamp;
};

/**
* Handles a clipboard instruction, which begins a new clipboard stream.
*
* @param {!string[]} args
* The arguments: [stream_index, mimetype]
*/
this.handleClipboard = function handleClipboard(args) {
var streamIndex = args[0];
var mimetype = args[1];

activeStreams[streamIndex] = {
mimetype: mimetype,
data: '',
timestamp: lastTimestamp
};
};

/**
* Handles a blob instruction, which contains base64-encoded data
* for an active stream.
*
* @param {!string[]} args
* The arguments: [stream_index, base64_data]
*/
this.handleBlob = function handleBlob(args) {
var streamIndex = args[0];
var base64Data = args[1];

var stream = activeStreams[streamIndex];
if (stream)
stream.data += base64Data;
};

/**
* Handles an end instruction, which completes a clipboard stream
* and creates the final clipboard event.
*
* @param {!string[]} args
* The arguments: [stream_index]
*/
this.handleEnd = function handleEnd(args) {
var streamIndex = args[0];

var stream = activeStreams[streamIndex];
if (stream) {
// Decode the base64 data
var decodedData = '';
try {
decodedData = atob(stream.data);
// Handle UTF-8 decoding
decodedData = decodeURIComponent(escape(decodedData));
} catch (e) {
// If decoding fails, use raw decoded data or mark as binary
try {
decodedData = atob(stream.data);
} catch (e2) {
decodedData = '[Binary data]';
}
}

// Create the clipboard event
parsedEvents.push(new Guacamole.ClipboardEventInterpreter.ClipboardEvent({
mimetype: stream.mimetype,
data: decodedData,
timestamp: stream.timestamp - startTimestamp
}));

// Clean up the stream
delete activeStreams[streamIndex];
}
};

/**
* Returns all parsed clipboard events.
*
* @returns {Guacamole.ClipboardEventInterpreter.ClipboardEvent[]}
* All clipboard events parsed so far.
*/
this.getEvents = function getEvents() {
return parsedEvents;
};

};

/**
* A single clipboard event from a recording.
*
* @constructor
* @param {Guacamole.ClipboardEventInterpreter.ClipboardEvent|object} [template={}]
* The object whose properties should be copied.
*/
Guacamole.ClipboardEventInterpreter.ClipboardEvent = function ClipboardEvent(template) {

template = template || {};

/**
* The mimetype of the clipboard data.
*
* @type {!string}
*/
this.mimetype = template.mimetype || 'text/plain';

/**
* The clipboard content (decoded from base64).
*
* @type {!string}
*/
this.data = template.data || '';

/**
* The timestamp when this clipboard event occurred, relative to
* the start of the recording.
*
* @type {!number}
*/
this.timestamp = template.timestamp || 0;

};
54 changes: 46 additions & 8 deletions guacamole-common-js/src/main/webapp/modules/SessionRecording.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,16 +428,24 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
var keyEventInterpreter = null;

/**
* Initialize the key interpreter. This function should be called only once
* with the first timestamp in the recording as an argument.
* A clipboard event interpreter to extract all clipboard events from
* this recording.
*
* @type {Guacamole.ClipboardEventInterpreter}
*/
var clipboardEventInterpreter = null;

/**
* Initialize the key and clipboard interpreters. This function should be
* called only once with the first timestamp in the recording.
*
* @private
* @param {!number} startTimestamp
* The timestamp of the first frame in the recording, i.e. the start of
* the recording.
* The timestamp of the first frame in the recording.
*/
function initializeKeyInterpreter(startTimestamp) {
function initializeInterpreters(startTimestamp) {
keyEventInterpreter = new Guacamole.KeyEventInterpreter(startTimestamp);
clipboardEventInterpreter = new Guacamole.ClipboardEventInterpreter(startTimestamp);
}

/**
Expand Down Expand Up @@ -466,6 +474,12 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
// Parse frame timestamp from sync instruction
var timestamp = parseInt(args[0]);

// Update the clipboard interpreter's timestamp so clipboard events
// are recorded with the correct time. Clipboard events don't have
// their own timestamps
if (clipboardEventInterpreter)
clipboardEventInterpreter.setTimestamp(timestamp);

// Add a new frame containing the instructions read since last frame
var frame = new Guacamole.SessionRecording._Frame(timestamp, frameStart, frameEnd);
frames.push(frame);
Expand All @@ -474,7 +488,7 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)
// If this is the first frame, intialize the key event interpreter
// with the timestamp of the first frame
if (frames.length === 1)
initializeKeyInterpreter(timestamp);
initializeInterpreters(timestamp);

// This frame should eventually become a keyframe if enough data
// has been processed and enough recording time has elapsed, or if
Expand All @@ -492,8 +506,27 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)

}

else if (opcode === 'key')
else if (opcode === 'key') {
keyEventInterpreter.handleKeyEvent(args);
// The clipboard gets updated on Ctrl+C key events so we update the
// clipboard interpreter's timestamp to match the timestamp as
// clipboard events don't have own timestamps
if (clipboardEventInterpreter) {
var keyTimestamp = parseInt(args[2]);
clipboardEventInterpreter.setTimestamp(keyTimestamp);
}
}

else if (opcode === 'clipboard' && clipboardEventInterpreter)
clipboardEventInterpreter.handleClipboard(args);

// Handle blob data (may be clipboard data)
else if (opcode === 'blob' && clipboardEventInterpreter)
clipboardEventInterpreter.handleBlob(args);

// Handle stream end (may complete clipboard stream)
else if (opcode === 'end' && clipboardEventInterpreter)
clipboardEventInterpreter.handleEnd(args);
};

/**
Expand Down Expand Up @@ -562,9 +595,14 @@ Guacamole.SessionRecording = function SessionRecording(source, refreshInterval)

// Now that the recording is fully processed, and all key events
// have been extracted, call the onkeyevents handler if defined
if (recording.onkeyevents)
if (recording.onkeyevents && keyEventInterpreter)
recording.onkeyevents(keyEventInterpreter.getEvents());

// call the onclipboardevents handler if defined with extracted
// clipboard events
if (recording.onclipboardevents && clipboardEventInterpreter)
recording.onclipboardevents(clipboardEventInterpreter.getEvents());

// Consider recording loaded if tunnel has closed without errors
if (!errorEncountered)
notifyLoaded();
Expand Down
36 changes: 33 additions & 3 deletions guacamole/src/main/frontend/src/app/player/directives/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,22 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
*/
var keyTimestamps = [];

/**
* Clipboard events extracted from the recording, stored for merging
* with key events.
*
* @type {!Guacamole.ClipboardEventInterpreter.ClipboardEvent[]}
*/
var clipboardEvents = [];

/**
* Key events extracted from the recording, stored for merging
* with clipboard events.
*
* @type {!Guacamole.KeyEventInterpreter.KeyEvent[]}
*/
var keyEvents = [];

/**
* Return true if any batches of key event logs are available for this
* recording, or false otherwise.
Expand Down Expand Up @@ -495,11 +511,25 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
// Extract key events from the recording
$scope.recording.onkeyevents = function keyEventsReceived(events) {

keyEvents = events;
keyTimestamps = events.map(event => event.timestamp);

// Convert to a display-optimized format
$scope.textBatches = (
keyEventDisplayService.parseEvents(events));
$scope.textBatches = keyEventDisplayService.parseEventsWithClipboard(
keyEvents, clipboardEvents
);

keyTimestamps = events.map(event => event.timestamp);
};

// Extract clipboard events from the recording
$scope.recording.onclipboardevents = function clipboardEventsReceived(events) {

clipboardEvents = events;

// Convert to a display-optimized format
$scope.textBatches = keyEventDisplayService.parseEventsWithClipboard(
keyEvents, clipboardEvents
);

};

Expand Down
Loading