Source: main/webapp/modules/Client.js

  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. var Guacamole = Guacamole || {};
  20. /**
  21. * Guacamole protocol client. Given a {@link Guacamole.Tunnel},
  22. * automatically handles incoming and outgoing Guacamole instructions via the
  23. * provided tunnel, updating its display using one or more canvas elements.
  24. *
  25. * @constructor
  26. * @param {!Guacamole.Tunnel} tunnel
  27. * The tunnel to use to send and receive Guacamole instructions.
  28. */
  29. Guacamole.Client = function(tunnel) {
  30. var guac_client = this;
  31. var STATE_IDLE = 0;
  32. var STATE_CONNECTING = 1;
  33. var STATE_WAITING = 2;
  34. var STATE_CONNECTED = 3;
  35. var STATE_DISCONNECTING = 4;
  36. var STATE_DISCONNECTED = 5;
  37. var currentState = STATE_IDLE;
  38. var currentTimestamp = 0;
  39. /**
  40. * The rough number of milliseconds to wait between sending keep-alive
  41. * pings. This may vary depending on how frequently the browser allows
  42. * timers to run, as well as how frequently the client receives messages
  43. * from the server.
  44. *
  45. * @private
  46. * @constant
  47. * @type {!number}
  48. */
  49. var KEEP_ALIVE_FREQUENCY = 5000;
  50. /**
  51. * The current keep-alive ping timeout ID, if any. This will only be set
  52. * upon connecting.
  53. *
  54. * @private
  55. * @type {number}
  56. */
  57. var keepAliveTimeout = null;
  58. /**
  59. * The timestamp of the point in time that the last keep-live ping was
  60. * sent, in milliseconds elapsed since midnight of January 1, 1970 UTC.
  61. *
  62. * @private
  63. * @type {!number}
  64. */
  65. var lastSentKeepAlive = 0;
  66. /**
  67. * Translation from Guacamole protocol line caps to Layer line caps.
  68. *
  69. * @private
  70. * @type {!Object.<number, string>}
  71. */
  72. var lineCap = {
  73. 0: "butt",
  74. 1: "round",
  75. 2: "square"
  76. };
  77. /**
  78. * Translation from Guacamole protocol line caps to Layer line caps.
  79. *
  80. * @private
  81. * @type {!Object.<number, string>}
  82. */
  83. var lineJoin = {
  84. 0: "bevel",
  85. 1: "miter",
  86. 2: "round"
  87. };
  88. /**
  89. * The underlying Guacamole display.
  90. *
  91. * @private
  92. * @type {!Guacamole.Display}
  93. */
  94. var display = new Guacamole.Display();
  95. /**
  96. * All available layers and buffers
  97. *
  98. * @private
  99. * @type {!Object.<number, (Guacamole.Display.VisibleLayer|Guacamole.Layer)>}
  100. */
  101. var layers = {};
  102. /**
  103. * All audio players currently in use by the client. Initially, this will
  104. * be empty, but audio players may be allocated by the server upon request.
  105. *
  106. * @private
  107. * @type {!Object.<number, Guacamole.AudioPlayer>}
  108. */
  109. var audioPlayers = {};
  110. /**
  111. * All video players currently in use by the client. Initially, this will
  112. * be empty, but video players may be allocated by the server upon request.
  113. *
  114. * @private
  115. * @type {!Object.<number, Guacamole.VideoPlayer>}
  116. */
  117. var videoPlayers = {};
  118. // No initial parsers
  119. var parsers = [];
  120. // No initial streams
  121. var streams = [];
  122. /**
  123. * All current objects. The index of each object is dictated by the
  124. * Guacamole server.
  125. *
  126. * @private
  127. * @type {!Guacamole.Object[]}
  128. */
  129. var objects = [];
  130. // Pool of available stream indices
  131. var stream_indices = new Guacamole.IntegerPool();
  132. // Array of allocated output streams by index
  133. var output_streams = [];
  134. function setState(state) {
  135. if (state != currentState) {
  136. currentState = state;
  137. if (guac_client.onstatechange)
  138. guac_client.onstatechange(currentState);
  139. }
  140. }
  141. function isConnected() {
  142. return currentState == STATE_CONNECTED
  143. || currentState == STATE_WAITING;
  144. }
  145. /**
  146. * Produces an opaque representation of Guacamole.Client state which can be
  147. * later imported through a call to importState(). This object is
  148. * effectively an independent, compressed snapshot of protocol and display
  149. * state. Invoking this function implicitly flushes the display.
  150. *
  151. * @param {!function} callback
  152. * Callback which should be invoked once the state object is ready. The
  153. * state object will be passed to the callback as the sole parameter.
  154. * This callback may be invoked immediately, or later as the display
  155. * finishes rendering and becomes ready.
  156. */
  157. this.exportState = function exportState(callback) {
  158. // Start with empty state
  159. var state = {
  160. 'currentState' : currentState,
  161. 'currentTimestamp' : currentTimestamp,
  162. 'layers' : {}
  163. };
  164. var layersSnapshot = {};
  165. // Make a copy of all current layers (protocol state)
  166. for (var key in layers) {
  167. layersSnapshot[key] = layers[key];
  168. }
  169. // Populate layers once data is available (display state, requires flush)
  170. display.flush(function populateLayers() {
  171. // Export each defined layer/buffer
  172. for (var key in layersSnapshot) {
  173. var index = parseInt(key);
  174. var layer = layersSnapshot[key];
  175. var canvas = layer.toCanvas();
  176. // Store layer/buffer dimensions
  177. var exportLayer = {
  178. 'width' : layer.width,
  179. 'height' : layer.height
  180. };
  181. // Store layer/buffer image data, if it can be generated
  182. if (layer.width && layer.height)
  183. exportLayer.url = canvas.toDataURL('image/png');
  184. // Add layer properties if not a buffer nor the default layer
  185. if (index > 0) {
  186. exportLayer.x = layer.x;
  187. exportLayer.y = layer.y;
  188. exportLayer.z = layer.z;
  189. exportLayer.alpha = layer.alpha;
  190. exportLayer.matrix = layer.matrix;
  191. exportLayer.parent = getLayerIndex(layer.parent);
  192. }
  193. // Store exported layer
  194. state.layers[key] = exportLayer;
  195. }
  196. // Invoke callback now that the state is ready
  197. callback(state);
  198. });
  199. };
  200. /**
  201. * Restores Guacamole.Client protocol and display state based on an opaque
  202. * object from a prior call to exportState(). The Guacamole.Client instance
  203. * used to export that state need not be the same as this instance.
  204. *
  205. * @param {!object} state
  206. * An opaque representation of Guacamole.Client state from a prior call
  207. * to exportState().
  208. *
  209. * @param {function} [callback]
  210. * The function to invoke when state has finished being imported. This
  211. * may happen immediately, or later as images within the provided state
  212. * object are loaded.
  213. */
  214. this.importState = function importState(state, callback) {
  215. var key;
  216. var index;
  217. currentState = state.currentState;
  218. currentTimestamp = state.currentTimestamp;
  219. // Cancel any pending display operations/frames
  220. display.cancel();
  221. // Dispose of all layers
  222. for (key in layers) {
  223. index = parseInt(key);
  224. if (index > 0)
  225. layers[key].dispose();
  226. }
  227. layers = {};
  228. // Import state of each layer/buffer
  229. for (key in state.layers) {
  230. index = parseInt(key);
  231. var importLayer = state.layers[key];
  232. var layer = getLayer(index);
  233. // Reset layer size
  234. display.resize(layer, importLayer.width, importLayer.height);
  235. // Initialize new layer if it has associated data
  236. if (importLayer.url) {
  237. display.setChannelMask(layer, Guacamole.Layer.SRC);
  238. display.draw(layer, 0, 0, importLayer.url);
  239. }
  240. // Set layer-specific properties if not a buffer nor the default layer
  241. if (index > 0 && importLayer.parent >= 0) {
  242. // Apply layer position and set parent
  243. var parent = getLayer(importLayer.parent);
  244. display.move(layer, parent, importLayer.x, importLayer.y, importLayer.z);
  245. // Set layer transparency
  246. display.shade(layer, importLayer.alpha);
  247. // Apply matrix transform
  248. var matrix = importLayer.matrix;
  249. display.distort(layer,
  250. matrix[0], matrix[1], matrix[2],
  251. matrix[3], matrix[4], matrix[5]);
  252. }
  253. }
  254. // Flush changes to display
  255. display.flush(callback);
  256. };
  257. /**
  258. * Returns the underlying display of this Guacamole.Client. The display
  259. * contains an Element which can be added to the DOM, causing the
  260. * display to become visible.
  261. *
  262. * @return {!Guacamole.Display}
  263. * The underlying display of this Guacamole.Client.
  264. */
  265. this.getDisplay = function() {
  266. return display;
  267. };
  268. /**
  269. * Sends the current size of the screen.
  270. *
  271. * @param {!number} width
  272. * The width of the screen.
  273. *
  274. * @param {!number} height
  275. * The height of the screen.
  276. */
  277. this.sendSize = function(width, height) {
  278. // Do not send requests if not connected
  279. if (!isConnected())
  280. return;
  281. tunnel.sendMessage("size", width, height);
  282. };
  283. /**
  284. * Sends a key event having the given properties as if the user
  285. * pressed or released a key.
  286. *
  287. * @param {!boolean} pressed
  288. * Whether the key is pressed (true) or released (false).
  289. *
  290. * @param {!number} keysym
  291. * The keysym of the key being pressed or released.
  292. */
  293. this.sendKeyEvent = function(pressed, keysym) {
  294. // Do not send requests if not connected
  295. if (!isConnected())
  296. return;
  297. tunnel.sendMessage("key", keysym, pressed);
  298. };
  299. /**
  300. * Sends a mouse event having the properties provided by the given mouse
  301. * state.
  302. *
  303. * @param {!Guacamole.Mouse.State} mouseState
  304. * The state of the mouse to send in the mouse event.
  305. *
  306. * @param {boolean} [applyDisplayScale=false]
  307. * Whether the provided mouse state uses local display units, rather
  308. * than remote display units, and should be scaled to match the
  309. * {@link Guacamole.Display}.
  310. */
  311. this.sendMouseState = function sendMouseState(mouseState, applyDisplayScale) {
  312. // Do not send requests if not connected
  313. if (!isConnected())
  314. return;
  315. var x = mouseState.x;
  316. var y = mouseState.y;
  317. // Translate for display units if requested
  318. if (applyDisplayScale) {
  319. x /= display.getScale();
  320. y /= display.getScale();
  321. }
  322. // Update client-side cursor
  323. display.moveCursor(
  324. Math.floor(x),
  325. Math.floor(y)
  326. );
  327. // Build mask
  328. var buttonMask = 0;
  329. if (mouseState.left) buttonMask |= 1;
  330. if (mouseState.middle) buttonMask |= 2;
  331. if (mouseState.right) buttonMask |= 4;
  332. if (mouseState.up) buttonMask |= 8;
  333. if (mouseState.down) buttonMask |= 16;
  334. // Send message
  335. tunnel.sendMessage("mouse", Math.floor(x), Math.floor(y), buttonMask);
  336. };
  337. /**
  338. * Sends a touch event having the properties provided by the given touch
  339. * state.
  340. *
  341. * @param {!Guacamole.Touch.State} touchState
  342. * The state of the touch contact to send in the touch event.
  343. *
  344. * @param {boolean} [applyDisplayScale=false]
  345. * Whether the provided touch state uses local display units, rather
  346. * than remote display units, and should be scaled to match the
  347. * {@link Guacamole.Display}.
  348. */
  349. this.sendTouchState = function sendTouchState(touchState, applyDisplayScale) {
  350. // Do not send requests if not connected
  351. if (!isConnected())
  352. return;
  353. var x = touchState.x;
  354. var y = touchState.y;
  355. // Translate for display units if requested
  356. if (applyDisplayScale) {
  357. x /= display.getScale();
  358. y /= display.getScale();
  359. }
  360. tunnel.sendMessage('touch', touchState.id, Math.floor(x), Math.floor(y),
  361. Math.floor(touchState.radiusX), Math.floor(touchState.radiusY),
  362. touchState.angle, touchState.force);
  363. };
  364. /**
  365. * Allocates an available stream index and creates a new
  366. * Guacamole.OutputStream using that index, associating the resulting
  367. * stream with this Guacamole.Client. Note that this stream will not yet
  368. * exist as far as the other end of the Guacamole connection is concerned.
  369. * Streams exist within the Guacamole protocol only when referenced by an
  370. * instruction which creates the stream, such as a "clipboard", "file", or
  371. * "pipe" instruction.
  372. *
  373. * @returns {!Guacamole.OutputStream}
  374. * A new Guacamole.OutputStream with a newly-allocated index and
  375. * associated with this Guacamole.Client.
  376. */
  377. this.createOutputStream = function createOutputStream() {
  378. // Allocate index
  379. var index = stream_indices.next();
  380. // Return new stream
  381. var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
  382. return stream;
  383. };
  384. /**
  385. * Opens a new audio stream for writing, where audio data having the give
  386. * mimetype will be sent along the returned stream. The instruction
  387. * necessary to create this stream will automatically be sent.
  388. *
  389. * @param {!string} mimetype
  390. * The mimetype of the audio data that will be sent along the returned
  391. * stream.
  392. *
  393. * @return {!Guacamole.OutputStream}
  394. * The created audio stream.
  395. */
  396. this.createAudioStream = function(mimetype) {
  397. // Allocate and associate stream with audio metadata
  398. var stream = guac_client.createOutputStream();
  399. tunnel.sendMessage("audio", stream.index, mimetype);
  400. return stream;
  401. };
  402. /**
  403. * Opens a new file for writing, having the given index, mimetype and
  404. * filename. The instruction necessary to create this stream will
  405. * automatically be sent.
  406. *
  407. * @param {!string} mimetype
  408. * The mimetype of the file being sent.
  409. *
  410. * @param {!string} filename
  411. * The filename of the file being sent.
  412. *
  413. * @return {!Guacamole.OutputStream}
  414. * The created file stream.
  415. */
  416. this.createFileStream = function(mimetype, filename) {
  417. // Allocate and associate stream with file metadata
  418. var stream = guac_client.createOutputStream();
  419. tunnel.sendMessage("file", stream.index, mimetype, filename);
  420. return stream;
  421. };
  422. /**
  423. * Opens a new pipe for writing, having the given name and mimetype. The
  424. * instruction necessary to create this stream will automatically be sent.
  425. *
  426. * @param {!string} mimetype
  427. * The mimetype of the data being sent.
  428. *
  429. * @param {!string} name
  430. * The name of the pipe.
  431. *
  432. * @return {!Guacamole.OutputStream}
  433. * The created file stream.
  434. */
  435. this.createPipeStream = function(mimetype, name) {
  436. // Allocate and associate stream with pipe metadata
  437. var stream = guac_client.createOutputStream();
  438. tunnel.sendMessage("pipe", stream.index, mimetype, name);
  439. return stream;
  440. };
  441. /**
  442. * Opens a new clipboard object for writing, having the given mimetype. The
  443. * instruction necessary to create this stream will automatically be sent.
  444. *
  445. * @param {!string} mimetype
  446. * The mimetype of the data being sent.
  447. *
  448. * @param {!string} name
  449. * The name of the pipe.
  450. *
  451. * @return {!Guacamole.OutputStream}
  452. * The created file stream.
  453. */
  454. this.createClipboardStream = function(mimetype) {
  455. // Allocate and associate stream with clipboard metadata
  456. var stream = guac_client.createOutputStream();
  457. tunnel.sendMessage("clipboard", stream.index, mimetype);
  458. return stream;
  459. };
  460. /**
  461. * Opens a new argument value stream for writing, having the given
  462. * parameter name and mimetype, requesting that the connection parameter
  463. * with the given name be updated to the value described by the contents
  464. * of the following stream. The instruction necessary to create this stream
  465. * will automatically be sent.
  466. *
  467. * @param {!string} mimetype
  468. * The mimetype of the data being sent.
  469. *
  470. * @param {!string} name
  471. * The name of the connection parameter to attempt to update.
  472. *
  473. * @return {!Guacamole.OutputStream}
  474. * The created argument value stream.
  475. */
  476. this.createArgumentValueStream = function createArgumentValueStream(mimetype, name) {
  477. // Allocate and associate stream with argument value metadata
  478. var stream = guac_client.createOutputStream();
  479. tunnel.sendMessage("argv", stream.index, mimetype, name);
  480. return stream;
  481. };
  482. /**
  483. * Creates a new output stream associated with the given object and having
  484. * the given mimetype and name. The legality of a mimetype and name is
  485. * dictated by the object itself. The instruction necessary to create this
  486. * stream will automatically be sent.
  487. *
  488. * @param {!number} index
  489. * The index of the object for which the output stream is being
  490. * created.
  491. *
  492. * @param {!string} mimetype
  493. * The mimetype of the data which will be sent to the output stream.
  494. *
  495. * @param {!string} name
  496. * The defined name of an output stream within the given object.
  497. *
  498. * @returns {!Guacamole.OutputStream}
  499. * An output stream which will write blobs to the named output stream
  500. * of the given object.
  501. */
  502. this.createObjectOutputStream = function createObjectOutputStream(index, mimetype, name) {
  503. // Allocate and associate stream with object metadata
  504. var stream = guac_client.createOutputStream();
  505. tunnel.sendMessage("put", index, stream.index, mimetype, name);
  506. return stream;
  507. };
  508. /**
  509. * Requests read access to the input stream having the given name. If
  510. * successful, a new input stream will be created.
  511. *
  512. * @param {!number} index
  513. * The index of the object from which the input stream is being
  514. * requested.
  515. *
  516. * @param {!string} name
  517. * The name of the input stream to request.
  518. */
  519. this.requestObjectInputStream = function requestObjectInputStream(index, name) {
  520. // Do not send requests if not connected
  521. if (!isConnected())
  522. return;
  523. tunnel.sendMessage("get", index, name);
  524. };
  525. /**
  526. * Acknowledge receipt of a blob on the stream with the given index.
  527. *
  528. * @param {!number} index
  529. * The index of the stream associated with the received blob.
  530. *
  531. * @param {!string} message
  532. * A human-readable message describing the error or status.
  533. *
  534. * @param {!number} code
  535. * The error code, if any, or 0 for success.
  536. */
  537. this.sendAck = function(index, message, code) {
  538. // Do not send requests if not connected
  539. if (!isConnected())
  540. return;
  541. tunnel.sendMessage("ack", index, message, code);
  542. };
  543. /**
  544. * Given the index of a file, writes a blob of data to that file.
  545. *
  546. * @param {!number} index
  547. * The index of the file to write to.
  548. *
  549. * @param {!string} data
  550. * Base64-encoded data to write to the file.
  551. */
  552. this.sendBlob = function(index, data) {
  553. // Do not send requests if not connected
  554. if (!isConnected())
  555. return;
  556. tunnel.sendMessage("blob", index, data);
  557. };
  558. /**
  559. * Marks a currently-open stream as complete. The other end of the
  560. * Guacamole connection will be notified via an "end" instruction that the
  561. * stream is closed, and the index will be made available for reuse in
  562. * future streams.
  563. *
  564. * @param {!number} index
  565. * The index of the stream to end.
  566. */
  567. this.endStream = function(index) {
  568. // Do not send requests if not connected
  569. if (!isConnected())
  570. return;
  571. // Explicitly close stream by sending "end" instruction
  572. tunnel.sendMessage("end", index);
  573. // Free associated index and stream if they exist
  574. if (output_streams[index]) {
  575. stream_indices.free(index);
  576. delete output_streams[index];
  577. }
  578. };
  579. /**
  580. * Fired whenever the state of this Guacamole.Client changes.
  581. *
  582. * @event
  583. * @param {!number} state
  584. * The new state of the client.
  585. */
  586. this.onstatechange = null;
  587. /**
  588. * Fired when the remote client sends a name update.
  589. *
  590. * @event
  591. * @param {!string} name
  592. * The new name of this client.
  593. */
  594. this.onname = null;
  595. /**
  596. * Fired when an error is reported by the remote client, and the connection
  597. * is being closed.
  598. *
  599. * @event
  600. * @param {!Guacamole.Status} status
  601. * A status object which describes the error.
  602. */
  603. this.onerror = null;
  604. /**
  605. * Fired when an arbitrary message is received from the tunnel that should
  606. * be processed by the client. By default, additional message-specific
  607. * events such as "onjoin" and "onleave" will fire for the received message
  608. * after this event has been processed. An event handler for "onmsg" need
  609. * not be supplied if "onjoin" and/or "onleave" will be used.
  610. *
  611. * @event
  612. * @param {!number} msgcode
  613. * A status code sent by the remote server that indicates the nature of
  614. * the message that is being sent to the client.
  615. *
  616. * @param {string[]} args
  617. * An array of arguments to be processed with the message sent to the
  618. * client.
  619. *
  620. * @return {boolean}
  621. * true if message-specific events such as "onjoin" and
  622. * "onleave" should be fired for this message, false otherwise. If
  623. * no value is returned, message-specific events will be allowed to
  624. * fire.
  625. */
  626. this.onmsg = null;
  627. /**
  628. * Fired when a user joins a shared connection.
  629. *
  630. * @event
  631. * @param {!string} userID
  632. * A unique value representing this specific user's connection to the
  633. * shared connection. This value is generated by the server and is
  634. * guaranteed to be unique relative to other users of the connection.
  635. *
  636. * @param {!string} name
  637. * A human-readable name representing the user that joined, such as
  638. * their username. This value is provided by the web application during
  639. * the connection handshake and is not necessarily unique relative to
  640. * other users of the connection.
  641. */
  642. this.onjoin = null;
  643. /**
  644. * Fired when a user leaves a shared connection.
  645. *
  646. * @event
  647. * @param {!string} userID
  648. * A unique value representing this specific user's connection to the
  649. * shared connection. This value is generated by the server and is
  650. * guaranteed to be unique relative to other users of the connection.
  651. *
  652. * @param {!string} name
  653. * A human-readable name representing the user that left, such as their
  654. * username. This value is provided by the web application during the
  655. * connection handshake and is not necessarily unique relative to other
  656. * users of the connection.
  657. */
  658. this.onleave = null;
  659. /**
  660. * Fired when a audio stream is created. The stream provided to this event
  661. * handler will contain its own event handlers for received data.
  662. *
  663. * @event
  664. * @param {!Guacamole.InputStream} stream
  665. * The stream that will receive audio data from the server.
  666. *
  667. * @param {!string} mimetype
  668. * The mimetype of the audio data which will be received.
  669. *
  670. * @return {Guacamole.AudioPlayer}
  671. * An object which implements the Guacamole.AudioPlayer interface and
  672. * has been initialized to play the data in the provided stream, or null
  673. * if the built-in audio players of the Guacamole client should be
  674. * used.
  675. */
  676. this.onaudio = null;
  677. /**
  678. * Fired when a video stream is created. The stream provided to this event
  679. * handler will contain its own event handlers for received data.
  680. *
  681. * @event
  682. * @param {!Guacamole.InputStream} stream
  683. * The stream that will receive video data from the server.
  684. *
  685. * @param {!Guacamole.Display.VisibleLayer} layer
  686. * The destination layer on which the received video data should be
  687. * played. It is the responsibility of the Guacamole.VideoPlayer
  688. * implementation to play the received data within this layer.
  689. *
  690. * @param {!string} mimetype
  691. * The mimetype of the video data which will be received.
  692. *
  693. * @return {Guacamole.VideoPlayer}
  694. * An object which implements the Guacamole.VideoPlayer interface and
  695. * has been initialized to play the data in the provided stream, or null
  696. * if the built-in video players of the Guacamole client should be
  697. * used.
  698. */
  699. this.onvideo = null;
  700. /**
  701. * Fired when the remote client is explicitly declaring the level of
  702. * multi-touch support provided by a particular display layer.
  703. *
  704. * @event
  705. * @param {!Guacamole.Display.VisibleLayer} layer
  706. * The layer whose multi-touch support level is being declared.
  707. *
  708. * @param {!number} touches
  709. * The maximum number of simultaneous touches supported by the given
  710. * layer, where 0 indicates that touch events are not supported at all.
  711. */
  712. this.onmultitouch = null;
  713. /**
  714. * Fired when the current value of a connection parameter is being exposed
  715. * by the server.
  716. *
  717. * @event
  718. * @param {!Guacamole.InputStream} stream
  719. * The stream that will receive connection parameter data from the
  720. * server.
  721. *
  722. * @param {!string} mimetype
  723. * The mimetype of the data which will be received.
  724. *
  725. * @param {!string} name
  726. * The name of the connection parameter whose value is being exposed.
  727. */
  728. this.onargv = null;
  729. /**
  730. * Fired when the clipboard of the remote client is changing.
  731. *
  732. * @event
  733. * @param {!Guacamole.InputStream} stream
  734. * The stream that will receive clipboard data from the server.
  735. *
  736. * @param {!string} mimetype
  737. * The mimetype of the data which will be received.
  738. */
  739. this.onclipboard = null;
  740. /**
  741. * Fired when a file stream is created. The stream provided to this event
  742. * handler will contain its own event handlers for received data.
  743. *
  744. * @event
  745. * @param {!Guacamole.InputStream} stream
  746. * The stream that will receive data from the server.
  747. *
  748. * @param {!string} mimetype
  749. * The mimetype of the file received.
  750. *
  751. * @param {!string} filename
  752. * The name of the file received.
  753. */
  754. this.onfile = null;
  755. /**
  756. * Fired when a filesystem object is created. The object provided to this
  757. * event handler will contain its own event handlers and functions for
  758. * requesting and handling data.
  759. *
  760. * @event
  761. * @param {!Guacamole.Object} object
  762. * The created filesystem object.
  763. *
  764. * @param {!string} name
  765. * The name of the filesystem.
  766. */
  767. this.onfilesystem = null;
  768. /**
  769. * Fired when a pipe stream is created. The stream provided to this event
  770. * handler will contain its own event handlers for received data;
  771. *
  772. * @event
  773. * @param {!Guacamole.InputStream} stream
  774. * The stream that will receive data from the server.
  775. *
  776. * @param {!string} mimetype
  777. * The mimetype of the data which will be received.
  778. *
  779. * @param {!string} name
  780. * The name of the pipe.
  781. */
  782. this.onpipe = null;
  783. /**
  784. * Fired when a "required" instruction is received. A required instruction
  785. * indicates that additional parameters are required for the connection to
  786. * continue, such as user credentials.
  787. *
  788. * @event
  789. * @param {!string[]} parameters
  790. * The names of the connection parameters that are required to be
  791. * provided for the connection to continue.
  792. */
  793. this.onrequired = null;
  794. /**
  795. * Fired whenever a sync instruction is received from the server, indicating
  796. * that the server is finished processing any input from the client and
  797. * has sent any results.
  798. *
  799. * @event
  800. * @param {!number} timestamp
  801. * The timestamp associated with the sync instruction.
  802. */
  803. this.onsync = null;
  804. /**
  805. * Returns the layer with the given index, creating it if necessary.
  806. * Positive indices refer to visible layers, an index of zero refers to
  807. * the default layer, and negative indices refer to buffers.
  808. *
  809. * @private
  810. * @param {!number} index
  811. * The index of the layer to retrieve.
  812. *
  813. * @return {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)}
  814. * The layer having the given index.
  815. */
  816. var getLayer = function getLayer(index) {
  817. // Get layer, create if necessary
  818. var layer = layers[index];
  819. if (!layer) {
  820. // Create layer based on index
  821. if (index === 0)
  822. layer = display.getDefaultLayer();
  823. else if (index > 0)
  824. layer = display.createLayer();
  825. else
  826. layer = display.createBuffer();
  827. // Add new layer
  828. layers[index] = layer;
  829. }
  830. return layer;
  831. };
  832. /**
  833. * Returns the index passed to getLayer() when the given layer was created.
  834. * Positive indices refer to visible layers, an index of zero refers to the
  835. * default layer, and negative indices refer to buffers.
  836. *
  837. * @param {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)} layer
  838. * The layer whose index should be determined.
  839. *
  840. * @returns {number}
  841. * The index of the given layer, or null if no such layer is associated
  842. * with this client.
  843. */
  844. var getLayerIndex = function getLayerIndex(layer) {
  845. // Avoid searching if there clearly is no such layer
  846. if (!layer)
  847. return null;
  848. // Search through each layer, returning the index of the given layer
  849. // once found
  850. for (var key in layers) {
  851. if (layer === layers[key])
  852. return parseInt(key);
  853. }
  854. // Otherwise, no such index
  855. return null;
  856. };
  857. function getParser(index) {
  858. var parser = parsers[index];
  859. // If parser not yet created, create it, and tie to the
  860. // oninstruction handler of the tunnel.
  861. if (parser == null) {
  862. parser = parsers[index] = new Guacamole.Parser();
  863. parser.oninstruction = tunnel.oninstruction;
  864. }
  865. return parser;
  866. }
  867. /**
  868. * Handlers for all defined layer properties.
  869. *
  870. * @private
  871. * @type {!Object.<string, function>}
  872. */
  873. var layerPropertyHandlers = {
  874. "miter-limit": function(layer, value) {
  875. display.setMiterLimit(layer, parseFloat(value));
  876. },
  877. "multi-touch" : function layerSupportsMultiTouch(layer, value) {
  878. // Process "multi-touch" property only for true visible layers (not off-screen buffers)
  879. if (guac_client.onmultitouch && layer instanceof Guacamole.Display.VisibleLayer)
  880. guac_client.onmultitouch(layer, parseInt(value));
  881. }
  882. };
  883. /**
  884. * Handlers for all instruction opcodes receivable by a Guacamole protocol
  885. * client.
  886. *
  887. * @private
  888. * @type {!Object.<string, function>}
  889. */
  890. var instructionHandlers = {
  891. "ack": function(parameters) {
  892. var stream_index = parseInt(parameters[0]);
  893. var reason = parameters[1];
  894. var code = parseInt(parameters[2]);
  895. // Get stream
  896. var stream = output_streams[stream_index];
  897. if (stream) {
  898. // Signal ack if handler defined
  899. if (stream.onack)
  900. stream.onack(new Guacamole.Status(code, reason));
  901. // If code is an error, invalidate stream if not already
  902. // invalidated by onack handler
  903. if (code >= 0x0100 && output_streams[stream_index] === stream) {
  904. stream_indices.free(stream_index);
  905. delete output_streams[stream_index];
  906. }
  907. }
  908. },
  909. "arc": function(parameters) {
  910. var layer = getLayer(parseInt(parameters[0]));
  911. var x = parseInt(parameters[1]);
  912. var y = parseInt(parameters[2]);
  913. var radius = parseInt(parameters[3]);
  914. var startAngle = parseFloat(parameters[4]);
  915. var endAngle = parseFloat(parameters[5]);
  916. var negative = parseInt(parameters[6]);
  917. display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0);
  918. },
  919. "argv": function(parameters) {
  920. var stream_index = parseInt(parameters[0]);
  921. var mimetype = parameters[1];
  922. var name = parameters[2];
  923. // Create stream
  924. if (guac_client.onargv) {
  925. var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
  926. guac_client.onargv(stream, mimetype, name);
  927. }
  928. // Otherwise, unsupported
  929. else
  930. guac_client.sendAck(stream_index, "Receiving argument values unsupported", 0x0100);
  931. },
  932. "audio": function(parameters) {
  933. var stream_index = parseInt(parameters[0]);
  934. var mimetype = parameters[1];
  935. // Create stream
  936. var stream = streams[stream_index] =
  937. new Guacamole.InputStream(guac_client, stream_index);
  938. // Get player instance via callback
  939. var audioPlayer = null;
  940. if (guac_client.onaudio)
  941. audioPlayer = guac_client.onaudio(stream, mimetype);
  942. // If unsuccessful, try to use a default implementation
  943. if (!audioPlayer)
  944. audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype);
  945. // If we have successfully retrieved an audio player, send success response
  946. if (audioPlayer) {
  947. audioPlayers[stream_index] = audioPlayer;
  948. guac_client.sendAck(stream_index, "OK", 0x0000);
  949. }
  950. // Otherwise, mimetype must be unsupported
  951. else
  952. guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);
  953. },
  954. "blob": function(parameters) {
  955. // Get stream
  956. var stream_index = parseInt(parameters[0]);
  957. var data = parameters[1];
  958. var stream = streams[stream_index];
  959. // Write data
  960. if (stream && stream.onblob)
  961. stream.onblob(data);
  962. },
  963. "body" : function handleBody(parameters) {
  964. // Get object
  965. var objectIndex = parseInt(parameters[0]);
  966. var object = objects[objectIndex];
  967. var streamIndex = parseInt(parameters[1]);
  968. var mimetype = parameters[2];
  969. var name = parameters[3];
  970. // Create stream if handler defined
  971. if (object && object.onbody) {
  972. var stream = streams[streamIndex] = new Guacamole.InputStream(guac_client, streamIndex);
  973. object.onbody(stream, mimetype, name);
  974. }
  975. // Otherwise, unsupported
  976. else
  977. guac_client.sendAck(streamIndex, "Receipt of body unsupported", 0x0100);
  978. },
  979. "cfill": function(parameters) {
  980. var channelMask = parseInt(parameters[0]);
  981. var layer = getLayer(parseInt(parameters[1]));
  982. var r = parseInt(parameters[2]);
  983. var g = parseInt(parameters[3]);
  984. var b = parseInt(parameters[4]);
  985. var a = parseInt(parameters[5]);
  986. display.setChannelMask(layer, channelMask);
  987. display.fillColor(layer, r, g, b, a);
  988. },
  989. "clip": function(parameters) {
  990. var layer = getLayer(parseInt(parameters[0]));
  991. display.clip(layer);
  992. },
  993. "clipboard": function(parameters) {
  994. var stream_index = parseInt(parameters[0]);
  995. var mimetype = parameters[1];
  996. // Create stream
  997. if (guac_client.onclipboard) {
  998. var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
  999. guac_client.onclipboard(stream, mimetype);
  1000. }
  1001. // Otherwise, unsupported
  1002. else
  1003. guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100);
  1004. },
  1005. "close": function(parameters) {
  1006. var layer = getLayer(parseInt(parameters[0]));
  1007. display.close(layer);
  1008. },
  1009. "copy": function(parameters) {
  1010. var srcL = getLayer(parseInt(parameters[0]));
  1011. var srcX = parseInt(parameters[1]);
  1012. var srcY = parseInt(parameters[2]);
  1013. var srcWidth = parseInt(parameters[3]);
  1014. var srcHeight = parseInt(parameters[4]);
  1015. var channelMask = parseInt(parameters[5]);
  1016. var dstL = getLayer(parseInt(parameters[6]));
  1017. var dstX = parseInt(parameters[7]);
  1018. var dstY = parseInt(parameters[8]);
  1019. display.setChannelMask(dstL, channelMask);
  1020. display.copy(srcL, srcX, srcY, srcWidth, srcHeight,
  1021. dstL, dstX, dstY);
  1022. },
  1023. "cstroke": function(parameters) {
  1024. var channelMask = parseInt(parameters[0]);
  1025. var layer = getLayer(parseInt(parameters[1]));
  1026. var cap = lineCap[parseInt(parameters[2])];
  1027. var join = lineJoin[parseInt(parameters[3])];
  1028. var thickness = parseInt(parameters[4]);
  1029. var r = parseInt(parameters[5]);
  1030. var g = parseInt(parameters[6]);
  1031. var b = parseInt(parameters[7]);
  1032. var a = parseInt(parameters[8]);
  1033. display.setChannelMask(layer, channelMask);
  1034. display.strokeColor(layer, cap, join, thickness, r, g, b, a);
  1035. },
  1036. "cursor": function(parameters) {
  1037. var cursorHotspotX = parseInt(parameters[0]);
  1038. var cursorHotspotY = parseInt(parameters[1]);
  1039. var srcL = getLayer(parseInt(parameters[2]));
  1040. var srcX = parseInt(parameters[3]);
  1041. var srcY = parseInt(parameters[4]);
  1042. var srcWidth = parseInt(parameters[5]);
  1043. var srcHeight = parseInt(parameters[6]);
  1044. display.setCursor(cursorHotspotX, cursorHotspotY,
  1045. srcL, srcX, srcY, srcWidth, srcHeight);
  1046. },
  1047. "curve": function(parameters) {
  1048. var layer = getLayer(parseInt(parameters[0]));
  1049. var cp1x = parseInt(parameters[1]);
  1050. var cp1y = parseInt(parameters[2]);
  1051. var cp2x = parseInt(parameters[3]);
  1052. var cp2y = parseInt(parameters[4]);
  1053. var x = parseInt(parameters[5]);
  1054. var y = parseInt(parameters[6]);
  1055. display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y);
  1056. },
  1057. "disconnect" : function handleDisconnect(parameters) {
  1058. // Explicitly tear down connection
  1059. guac_client.disconnect();
  1060. },
  1061. "dispose": function(parameters) {
  1062. var layer_index = parseInt(parameters[0]);
  1063. // If visible layer, remove from parent
  1064. if (layer_index > 0) {
  1065. // Remove from parent
  1066. var layer = getLayer(layer_index);
  1067. display.dispose(layer);
  1068. // Delete reference
  1069. delete layers[layer_index];
  1070. }
  1071. // If buffer, just delete reference
  1072. else if (layer_index < 0)
  1073. delete layers[layer_index];
  1074. // Attempting to dispose the root layer currently has no effect.
  1075. },
  1076. "distort": function(parameters) {
  1077. var layer_index = parseInt(parameters[0]);
  1078. var a = parseFloat(parameters[1]);
  1079. var b = parseFloat(parameters[2]);
  1080. var c = parseFloat(parameters[3]);
  1081. var d = parseFloat(parameters[4]);
  1082. var e = parseFloat(parameters[5]);
  1083. var f = parseFloat(parameters[6]);
  1084. // Only valid for visible layers (not buffers)
  1085. if (layer_index >= 0) {
  1086. var layer = getLayer(layer_index);
  1087. display.distort(layer, a, b, c, d, e, f);
  1088. }
  1089. },
  1090. "error": function(parameters) {
  1091. var reason = parameters[0];
  1092. var code = parseInt(parameters[1]);
  1093. // Call handler if defined
  1094. if (guac_client.onerror)
  1095. guac_client.onerror(new Guacamole.Status(code, reason));
  1096. guac_client.disconnect();
  1097. },
  1098. "end": function(parameters) {
  1099. var stream_index = parseInt(parameters[0]);
  1100. // Get stream
  1101. var stream = streams[stream_index];
  1102. if (stream) {
  1103. // Signal end of stream if handler defined
  1104. if (stream.onend)
  1105. stream.onend();
  1106. // Invalidate stream
  1107. delete streams[stream_index];
  1108. }
  1109. },
  1110. "file": function(parameters) {
  1111. var stream_index = parseInt(parameters[0]);
  1112. var mimetype = parameters[1];
  1113. var filename = parameters[2];
  1114. // Create stream
  1115. if (guac_client.onfile) {
  1116. var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
  1117. guac_client.onfile(stream, mimetype, filename);
  1118. }
  1119. // Otherwise, unsupported
  1120. else
  1121. guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100);
  1122. },
  1123. "filesystem" : function handleFilesystem(parameters) {
  1124. var objectIndex = parseInt(parameters[0]);
  1125. var name = parameters[1];
  1126. // Create object, if supported
  1127. if (guac_client.onfilesystem) {
  1128. var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex);
  1129. guac_client.onfilesystem(object, name);
  1130. }
  1131. // If unsupported, simply ignore the availability of the filesystem
  1132. },
  1133. "identity": function(parameters) {
  1134. var layer = getLayer(parseInt(parameters[0]));
  1135. display.setTransform(layer, 1, 0, 0, 1, 0, 0);
  1136. },
  1137. "img": function(parameters) {
  1138. var stream_index = parseInt(parameters[0]);
  1139. var channelMask = parseInt(parameters[1]);
  1140. var layer = getLayer(parseInt(parameters[2]));
  1141. var mimetype = parameters[3];
  1142. var x = parseInt(parameters[4]);
  1143. var y = parseInt(parameters[5]);
  1144. // Create stream
  1145. var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
  1146. // Draw received contents once decoded
  1147. display.setChannelMask(layer, channelMask);
  1148. display.drawStream(layer, x, y, stream, mimetype);
  1149. },
  1150. "jpeg": function(parameters) {
  1151. var channelMask = parseInt(parameters[0]);
  1152. var layer = getLayer(parseInt(parameters[1]));
  1153. var x = parseInt(parameters[2]);
  1154. var y = parseInt(parameters[3]);
  1155. var data = parameters[4];
  1156. display.setChannelMask(layer, channelMask);
  1157. display.draw(layer, x, y, "data:image/jpeg;base64," + data);
  1158. },
  1159. "lfill": function(parameters) {
  1160. var channelMask = parseInt(parameters[0]);
  1161. var layer = getLayer(parseInt(parameters[1]));
  1162. var srcLayer = getLayer(parseInt(parameters[2]));
  1163. display.setChannelMask(layer, channelMask);
  1164. display.fillLayer(layer, srcLayer);
  1165. },
  1166. "line": function(parameters) {
  1167. var layer = getLayer(parseInt(parameters[0]));
  1168. var x = parseInt(parameters[1]);
  1169. var y = parseInt(parameters[2]);
  1170. display.lineTo(layer, x, y);
  1171. },
  1172. "lstroke": function(parameters) {
  1173. var channelMask = parseInt(parameters[0]);
  1174. var layer = getLayer(parseInt(parameters[1]));
  1175. var srcLayer = getLayer(parseInt(parameters[2]));
  1176. display.setChannelMask(layer, channelMask);
  1177. display.strokeLayer(layer, srcLayer);
  1178. },
  1179. "mouse" : function handleMouse(parameters) {
  1180. var x = parseInt(parameters[0]);
  1181. var y = parseInt(parameters[1]);
  1182. // Display and move software cursor to received coordinates
  1183. display.showCursor(true);
  1184. display.moveCursor(x, y);
  1185. },
  1186. "move": function(parameters) {
  1187. var layer_index = parseInt(parameters[0]);
  1188. var parent_index = parseInt(parameters[1]);
  1189. var x = parseInt(parameters[2]);
  1190. var y = parseInt(parameters[3]);
  1191. var z = parseInt(parameters[4]);
  1192. // Only valid for non-default layers
  1193. if (layer_index > 0 && parent_index >= 0) {
  1194. var layer = getLayer(layer_index);
  1195. var parent = getLayer(parent_index);
  1196. display.move(layer, parent, x, y, z);
  1197. }
  1198. },
  1199. "msg" : function(parameters) {
  1200. var userID;
  1201. var username;
  1202. // Fire general message handling event first
  1203. var allowDefault = true;
  1204. var msgid = parseInt(parameters[0]);
  1205. if (guac_client.onmsg) {
  1206. allowDefault = guac_client.onmsg(msgid, parameters.slice(1));
  1207. if (allowDefault === undefined)
  1208. allowDefault = true;
  1209. }
  1210. // Fire message-specific convenience events if not prevented by the
  1211. // "onmsg" handler
  1212. if (allowDefault) {
  1213. switch (msgid) {
  1214. case Guacamole.Client.Message.USER_JOINED:
  1215. userID = parameters[1];
  1216. username = parameters[2];
  1217. if (guac_client.onjoin)
  1218. guac_client.onjoin(userID, username);
  1219. break;
  1220. case Guacamole.Client.Message.USER_LEFT:
  1221. userID = parameters[1];
  1222. username = parameters[2];
  1223. if (guac_client.onleave)
  1224. guac_client.onleave(userID, username);
  1225. break;
  1226. }
  1227. }
  1228. },
  1229. "name": function(parameters) {
  1230. if (guac_client.onname) guac_client.onname(parameters[0]);
  1231. },
  1232. "nest": function(parameters) {
  1233. var parser = getParser(parseInt(parameters[0]));
  1234. parser.receive(parameters[1]);
  1235. },
  1236. "pipe": function(parameters) {
  1237. var stream_index = parseInt(parameters[0]);
  1238. var mimetype = parameters[1];
  1239. var name = parameters[2];
  1240. // Create stream
  1241. if (guac_client.onpipe) {
  1242. var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
  1243. guac_client.onpipe(stream, mimetype, name);
  1244. }
  1245. // Otherwise, unsupported
  1246. else
  1247. guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100);
  1248. },
  1249. "png": function(parameters) {
  1250. var channelMask = parseInt(parameters[0]);
  1251. var layer = getLayer(parseInt(parameters[1]));
  1252. var x = parseInt(parameters[2]);
  1253. var y = parseInt(parameters[3]);
  1254. var data = parameters[4];
  1255. display.setChannelMask(layer, channelMask);
  1256. display.draw(layer, x, y, "data:image/png;base64," + data);
  1257. },
  1258. "pop": function(parameters) {
  1259. var layer = getLayer(parseInt(parameters[0]));
  1260. display.pop(layer);
  1261. },
  1262. "push": function(parameters) {
  1263. var layer = getLayer(parseInt(parameters[0]));
  1264. display.push(layer);
  1265. },
  1266. "rect": function(parameters) {
  1267. var layer = getLayer(parseInt(parameters[0]));
  1268. var x = parseInt(parameters[1]);
  1269. var y = parseInt(parameters[2]);
  1270. var w = parseInt(parameters[3]);
  1271. var h = parseInt(parameters[4]);
  1272. display.rect(layer, x, y, w, h);
  1273. },
  1274. "required": function required(parameters) {
  1275. if (guac_client.onrequired) guac_client.onrequired(parameters);
  1276. },
  1277. "reset": function(parameters) {
  1278. var layer = getLayer(parseInt(parameters[0]));
  1279. display.reset(layer);
  1280. },
  1281. "set": function(parameters) {
  1282. var layer = getLayer(parseInt(parameters[0]));
  1283. var name = parameters[1];
  1284. var value = parameters[2];
  1285. // Call property handler if defined
  1286. var handler = layerPropertyHandlers[name];
  1287. if (handler)
  1288. handler(layer, value);
  1289. },
  1290. "shade": function(parameters) {
  1291. var layer_index = parseInt(parameters[0]);
  1292. var a = parseInt(parameters[1]);
  1293. // Only valid for visible layers (not buffers)
  1294. if (layer_index >= 0) {
  1295. var layer = getLayer(layer_index);
  1296. display.shade(layer, a);
  1297. }
  1298. },
  1299. "size": function(parameters) {
  1300. var layer_index = parseInt(parameters[0]);
  1301. var layer = getLayer(layer_index);
  1302. var width = parseInt(parameters[1]);
  1303. var height = parseInt(parameters[2]);
  1304. display.resize(layer, width, height);
  1305. },
  1306. "start": function(parameters) {
  1307. var layer = getLayer(parseInt(parameters[0]));
  1308. var x = parseInt(parameters[1]);
  1309. var y = parseInt(parameters[2]);
  1310. display.moveTo(layer, x, y);
  1311. },
  1312. "sync": function(parameters) {
  1313. var timestamp = parseInt(parameters[0]);
  1314. // Flush display, send sync when done
  1315. display.flush(function displaySyncComplete() {
  1316. // Synchronize all audio players
  1317. for (var index in audioPlayers) {
  1318. var audioPlayer = audioPlayers[index];
  1319. if (audioPlayer)
  1320. audioPlayer.sync();
  1321. }
  1322. // Send sync response to server
  1323. if (timestamp !== currentTimestamp) {
  1324. tunnel.sendMessage("sync", timestamp);
  1325. currentTimestamp = timestamp;
  1326. }
  1327. });
  1328. // If received first update, no longer waiting.
  1329. if (currentState === STATE_WAITING)
  1330. setState(STATE_CONNECTED);
  1331. // Call sync handler if defined
  1332. if (guac_client.onsync)
  1333. guac_client.onsync(timestamp);
  1334. },
  1335. "transfer": function(parameters) {
  1336. var srcL = getLayer(parseInt(parameters[0]));
  1337. var srcX = parseInt(parameters[1]);
  1338. var srcY = parseInt(parameters[2]);
  1339. var srcWidth = parseInt(parameters[3]);
  1340. var srcHeight = parseInt(parameters[4]);
  1341. var function_index = parseInt(parameters[5]);
  1342. var dstL = getLayer(parseInt(parameters[6]));
  1343. var dstX = parseInt(parameters[7]);
  1344. var dstY = parseInt(parameters[8]);
  1345. /* SRC */
  1346. if (function_index === 0x3)
  1347. display.put(srcL, srcX, srcY, srcWidth, srcHeight,
  1348. dstL, dstX, dstY);
  1349. /* Anything else that isn't a NO-OP */
  1350. else if (function_index !== 0x5)
  1351. display.transfer(srcL, srcX, srcY, srcWidth, srcHeight,
  1352. dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]);
  1353. },
  1354. "transform": function(parameters) {
  1355. var layer = getLayer(parseInt(parameters[0]));
  1356. var a = parseFloat(parameters[1]);
  1357. var b = parseFloat(parameters[2]);
  1358. var c = parseFloat(parameters[3]);
  1359. var d = parseFloat(parameters[4]);
  1360. var e = parseFloat(parameters[5]);
  1361. var f = parseFloat(parameters[6]);
  1362. display.transform(layer, a, b, c, d, e, f);
  1363. },
  1364. "undefine" : function handleUndefine(parameters) {
  1365. // Get object
  1366. var objectIndex = parseInt(parameters[0]);
  1367. var object = objects[objectIndex];
  1368. // Signal end of object definition
  1369. if (object && object.onundefine)
  1370. object.onundefine();
  1371. },
  1372. "video": function(parameters) {
  1373. var stream_index = parseInt(parameters[0]);
  1374. var layer = getLayer(parseInt(parameters[1]));
  1375. var mimetype = parameters[2];
  1376. // Create stream
  1377. var stream = streams[stream_index] =
  1378. new Guacamole.InputStream(guac_client, stream_index);
  1379. // Get player instance via callback
  1380. var videoPlayer = null;
  1381. if (guac_client.onvideo)
  1382. videoPlayer = guac_client.onvideo(stream, layer, mimetype);
  1383. // If unsuccessful, try to use a default implementation
  1384. if (!videoPlayer)
  1385. videoPlayer = Guacamole.VideoPlayer.getInstance(stream, layer, mimetype);
  1386. // If we have successfully retrieved an video player, send success response
  1387. if (videoPlayer) {
  1388. videoPlayers[stream_index] = videoPlayer;
  1389. guac_client.sendAck(stream_index, "OK", 0x0000);
  1390. }
  1391. // Otherwise, mimetype must be unsupported
  1392. else
  1393. guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);
  1394. }
  1395. };
  1396. /**
  1397. * Sends a keep-alive ping to the Guacamole server, advising the server
  1398. * that the client is still connected and responding. The lastSentKeepAlive
  1399. * timestamp is automatically updated as a result of calling this function.
  1400. *
  1401. * @private
  1402. */
  1403. var sendKeepAlive = function sendKeepAlive() {
  1404. tunnel.sendMessage('nop');
  1405. lastSentKeepAlive = new Date().getTime();
  1406. };
  1407. /**
  1408. * Schedules the next keep-alive ping based on the KEEP_ALIVE_FREQUENCY and
  1409. * the time that the last ping was sent, if ever. If enough time has
  1410. * elapsed that a ping should have already been sent, calling this function
  1411. * will send that ping immediately.
  1412. *
  1413. * @private
  1414. */
  1415. var scheduleKeepAlive = function scheduleKeepAlive() {
  1416. window.clearTimeout(keepAliveTimeout);
  1417. var currentTime = new Date().getTime();
  1418. var keepAliveDelay = Math.max(lastSentKeepAlive + KEEP_ALIVE_FREQUENCY - currentTime, 0);
  1419. // Ping server regularly to keep connection alive, but send the ping
  1420. // immediately if enough time has elapsed that it should have already
  1421. // been sent
  1422. if (keepAliveDelay > 0)
  1423. keepAliveTimeout = window.setTimeout(sendKeepAlive, keepAliveDelay);
  1424. else
  1425. sendKeepAlive();
  1426. };
  1427. /**
  1428. * Stops sending any further keep-alive pings. If a keep-alive ping was
  1429. * scheduled to be sent, that ping is cancelled.
  1430. *
  1431. * @private
  1432. */
  1433. var stopKeepAlive = function stopKeepAlive() {
  1434. window.clearTimeout(keepAliveTimeout);
  1435. };
  1436. tunnel.oninstruction = function(opcode, parameters) {
  1437. var handler = instructionHandlers[opcode];
  1438. if (handler)
  1439. handler(parameters);
  1440. // Leverage network activity to ensure the next keep-alive ping is
  1441. // sent, even if the browser is currently throttling timers
  1442. scheduleKeepAlive();
  1443. };
  1444. /**
  1445. * Sends a disconnect instruction to the server and closes the tunnel.
  1446. */
  1447. this.disconnect = function() {
  1448. // Only attempt disconnection not disconnected.
  1449. if (currentState != STATE_DISCONNECTED
  1450. && currentState != STATE_DISCONNECTING) {
  1451. setState(STATE_DISCONNECTING);
  1452. // Stop sending keep-alive messages
  1453. stopKeepAlive();
  1454. // Send disconnect message and disconnect
  1455. tunnel.sendMessage("disconnect");
  1456. tunnel.disconnect();
  1457. setState(STATE_DISCONNECTED);
  1458. }
  1459. };
  1460. /**
  1461. * Connects the underlying tunnel of this Guacamole.Client, passing the
  1462. * given arbitrary data to the tunnel during the connection process.
  1463. *
  1464. * @param {string} data
  1465. * Arbitrary connection data to be sent to the underlying tunnel during
  1466. * the connection process.
  1467. *
  1468. * @throws {!Guacamole.Status}
  1469. * If an error occurs during connection.
  1470. */
  1471. this.connect = function(data) {
  1472. setState(STATE_CONNECTING);
  1473. try {
  1474. tunnel.connect(data);
  1475. }
  1476. catch (status) {
  1477. setState(STATE_IDLE);
  1478. throw status;
  1479. }
  1480. // Regularly send keep-alive ping to ensure the server knows we're
  1481. // still here, even if not active
  1482. scheduleKeepAlive();
  1483. setState(STATE_WAITING);
  1484. };
  1485. };
  1486. /**
  1487. * Map of all Guacamole binary raster operations to transfer functions.
  1488. *
  1489. * @private
  1490. * @type {!Object.<number, function>}
  1491. */
  1492. Guacamole.Client.DefaultTransferFunction = {
  1493. /* BLACK */
  1494. 0x0: function (src, dst) {
  1495. dst.red = dst.green = dst.blue = 0x00;
  1496. },
  1497. /* WHITE */
  1498. 0xF: function (src, dst) {
  1499. dst.red = dst.green = dst.blue = 0xFF;
  1500. },
  1501. /* SRC */
  1502. 0x3: function (src, dst) {
  1503. dst.red = src.red;
  1504. dst.green = src.green;
  1505. dst.blue = src.blue;
  1506. dst.alpha = src.alpha;
  1507. },
  1508. /* DEST (no-op) */
  1509. 0x5: function (src, dst) {
  1510. // Do nothing
  1511. },
  1512. /* Invert SRC */
  1513. 0xC: function (src, dst) {
  1514. dst.red = 0xFF & ~src.red;
  1515. dst.green = 0xFF & ~src.green;
  1516. dst.blue = 0xFF & ~src.blue;
  1517. dst.alpha = src.alpha;
  1518. },
  1519. /* Invert DEST */
  1520. 0xA: function (src, dst) {
  1521. dst.red = 0xFF & ~dst.red;
  1522. dst.green = 0xFF & ~dst.green;
  1523. dst.blue = 0xFF & ~dst.blue;
  1524. },
  1525. /* AND */
  1526. 0x1: function (src, dst) {
  1527. dst.red = ( src.red & dst.red);
  1528. dst.green = ( src.green & dst.green);
  1529. dst.blue = ( src.blue & dst.blue);
  1530. },
  1531. /* NAND */
  1532. 0xE: function (src, dst) {
  1533. dst.red = 0xFF & ~( src.red & dst.red);
  1534. dst.green = 0xFF & ~( src.green & dst.green);
  1535. dst.blue = 0xFF & ~( src.blue & dst.blue);
  1536. },
  1537. /* OR */
  1538. 0x7: function (src, dst) {
  1539. dst.red = ( src.red | dst.red);
  1540. dst.green = ( src.green | dst.green);
  1541. dst.blue = ( src.blue | dst.blue);
  1542. },
  1543. /* NOR */
  1544. 0x8: function (src, dst) {
  1545. dst.red = 0xFF & ~( src.red | dst.red);
  1546. dst.green = 0xFF & ~( src.green | dst.green);
  1547. dst.blue = 0xFF & ~( src.blue | dst.blue);
  1548. },
  1549. /* XOR */
  1550. 0x6: function (src, dst) {
  1551. dst.red = ( src.red ^ dst.red);
  1552. dst.green = ( src.green ^ dst.green);
  1553. dst.blue = ( src.blue ^ dst.blue);
  1554. },
  1555. /* XNOR */
  1556. 0x9: function (src, dst) {
  1557. dst.red = 0xFF & ~( src.red ^ dst.red);
  1558. dst.green = 0xFF & ~( src.green ^ dst.green);
  1559. dst.blue = 0xFF & ~( src.blue ^ dst.blue);
  1560. },
  1561. /* AND inverted source */
  1562. 0x4: function (src, dst) {
  1563. dst.red = 0xFF & (~src.red & dst.red);
  1564. dst.green = 0xFF & (~src.green & dst.green);
  1565. dst.blue = 0xFF & (~src.blue & dst.blue);
  1566. },
  1567. /* OR inverted source */
  1568. 0xD: function (src, dst) {
  1569. dst.red = 0xFF & (~src.red | dst.red);
  1570. dst.green = 0xFF & (~src.green | dst.green);
  1571. dst.blue = 0xFF & (~src.blue | dst.blue);
  1572. },
  1573. /* AND inverted destination */
  1574. 0x2: function (src, dst) {
  1575. dst.red = 0xFF & ( src.red & ~dst.red);
  1576. dst.green = 0xFF & ( src.green & ~dst.green);
  1577. dst.blue = 0xFF & ( src.blue & ~dst.blue);
  1578. },
  1579. /* OR inverted destination */
  1580. 0xB: function (src, dst) {
  1581. dst.red = 0xFF & ( src.red | ~dst.red);
  1582. dst.green = 0xFF & ( src.green | ~dst.green);
  1583. dst.blue = 0xFF & ( src.blue | ~dst.blue);
  1584. }
  1585. };
  1586. /**
  1587. * A list of possible messages that can be sent by the server for processing
  1588. * by the client.
  1589. *
  1590. * @type {!Object.<string, number>}
  1591. */
  1592. Guacamole.Client.Message = {
  1593. /**
  1594. * A client message that indicates that a user has joined an existing
  1595. * connection. This message expects a single additional argument - the
  1596. * name of the user who has joined the connection.
  1597. *
  1598. * @type {!number}
  1599. */
  1600. "USER_JOINED": 0x0001,
  1601. /**
  1602. * A client message that indicates that a user has left an existing
  1603. * connection. This message expects a single additional argument - the
  1604. * name of the user who has left the connection.
  1605. *
  1606. * @type {!number}
  1607. */
  1608. "USER_LEFT": 0x0002
  1609. };