Source: main/webapp/modules/Tunnel.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. * Core object providing abstract communication for Guacamole. This object
  22. * is a null implementation whose functions do nothing. Guacamole applications
  23. * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
  24. * on this one.
  25. *
  26. * @constructor
  27. * @see Guacamole.HTTPTunnel
  28. */
  29. Guacamole.Tunnel = function() {
  30. /**
  31. * Connect to the tunnel with the given optional data. This data is
  32. * typically used for authentication. The format of data accepted is
  33. * up to the tunnel implementation.
  34. *
  35. * @param {string} [data]
  36. * The data to send to the tunnel when connecting.
  37. */
  38. this.connect = function(data) {};
  39. /**
  40. * Disconnect from the tunnel.
  41. */
  42. this.disconnect = function() {};
  43. /**
  44. * Send the given message through the tunnel to the service on the other
  45. * side. All messages are guaranteed to be received in the order sent.
  46. *
  47. * @param {...*} elements
  48. * The elements of the message to send to the service on the other side
  49. * of the tunnel.
  50. */
  51. this.sendMessage = function(elements) {};
  52. /**
  53. * Changes the stored numeric state of this tunnel, firing the onstatechange
  54. * event if the new state is different and a handler has been defined.
  55. *
  56. * @private
  57. * @param {!number} state
  58. * The new state of this tunnel.
  59. */
  60. this.setState = function(state) {
  61. // Notify only if state changes
  62. if (state !== this.state) {
  63. this.state = state;
  64. if (this.onstatechange)
  65. this.onstatechange(state);
  66. }
  67. };
  68. /**
  69. * Changes the stored UUID that uniquely identifies this tunnel, firing the
  70. * onuuid event if a handler has been defined.
  71. *
  72. * @private
  73. * @param {string} uuid
  74. * The new state of this tunnel.
  75. */
  76. this.setUUID = function setUUID(uuid) {
  77. this.uuid = uuid;
  78. if (this.onuuid)
  79. this.onuuid(uuid);
  80. };
  81. /**
  82. * Returns whether this tunnel is currently connected.
  83. *
  84. * @returns {!boolean}
  85. * true if this tunnel is currently connected, false otherwise.
  86. */
  87. this.isConnected = function isConnected() {
  88. return this.state === Guacamole.Tunnel.State.OPEN
  89. || this.state === Guacamole.Tunnel.State.UNSTABLE;
  90. };
  91. /**
  92. * The current state of this tunnel.
  93. *
  94. * @type {!number}
  95. */
  96. this.state = Guacamole.Tunnel.State.CLOSED;
  97. /**
  98. * The maximum amount of time to wait for data to be received, in
  99. * milliseconds. If data is not received within this amount of time,
  100. * the tunnel is closed with an error. The default value is 15000.
  101. *
  102. * @type {!number}
  103. */
  104. this.receiveTimeout = 15000;
  105. /**
  106. * The amount of time to wait for data to be received before considering
  107. * the connection to be unstable, in milliseconds. If data is not received
  108. * within this amount of time, the tunnel status is updated to warn that
  109. * the connection appears unresponsive and may close. The default value is
  110. * 1500.
  111. *
  112. * @type {!number}
  113. */
  114. this.unstableThreshold = 1500;
  115. /**
  116. * The UUID uniquely identifying this tunnel. If not yet known, this will
  117. * be null.
  118. *
  119. * @type {string}
  120. */
  121. this.uuid = null;
  122. /**
  123. * Fired when the UUID that uniquely identifies this tunnel is known.
  124. *
  125. * @event
  126. * @param {!string}
  127. * The UUID uniquely identifying this tunnel.
  128. */
  129. this.onuuid = null;
  130. /**
  131. * Fired whenever an error is encountered by the tunnel.
  132. *
  133. * @event
  134. * @param {!Guacamole.Status} status
  135. * A status object which describes the error.
  136. */
  137. this.onerror = null;
  138. /**
  139. * Fired whenever the state of the tunnel changes.
  140. *
  141. * @event
  142. * @param {!number} state
  143. * The new state of the client.
  144. */
  145. this.onstatechange = null;
  146. /**
  147. * Fired once for every complete Guacamole instruction received, in order.
  148. *
  149. * @event
  150. * @param {!string} opcode
  151. * The Guacamole instruction opcode.
  152. *
  153. * @param {!string[]} parameters
  154. * The parameters provided for the instruction, if any.
  155. */
  156. this.oninstruction = null;
  157. };
  158. /**
  159. * The Guacamole protocol instruction opcode reserved for arbitrary internal
  160. * use by tunnel implementations. The value of this opcode is guaranteed to be
  161. * the empty string (""). Tunnel implementations may use this opcode for any
  162. * purpose. It is currently used by the HTTP tunnel to mark the end of the HTTP
  163. * response, and by the WebSocket tunnel to transmit the tunnel UUID and send
  164. * connection stability test pings/responses.
  165. *
  166. * @constant
  167. * @type {!string}
  168. */
  169. Guacamole.Tunnel.INTERNAL_DATA_OPCODE = '';
  170. /**
  171. * All possible tunnel states.
  172. *
  173. * @type {!Object.<string, number>}
  174. */
  175. Guacamole.Tunnel.State = {
  176. /**
  177. * A connection is in pending. It is not yet known whether connection was
  178. * successful.
  179. *
  180. * @type {!number}
  181. */
  182. "CONNECTING": 0,
  183. /**
  184. * Connection was successful, and data is being received.
  185. *
  186. * @type {!number}
  187. */
  188. "OPEN": 1,
  189. /**
  190. * The connection is closed. Connection may not have been successful, the
  191. * tunnel may have been explicitly closed by either side, or an error may
  192. * have occurred.
  193. *
  194. * @type {!number}
  195. */
  196. "CLOSED": 2,
  197. /**
  198. * The connection is open, but communication through the tunnel appears to
  199. * be disrupted, and the connection may close as a result.
  200. *
  201. * @type {!number}
  202. */
  203. "UNSTABLE" : 3
  204. };
  205. /**
  206. * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
  207. *
  208. * @constructor
  209. * @augments Guacamole.Tunnel
  210. *
  211. * @param {!string} tunnelURL
  212. * The URL of the HTTP tunneling service.
  213. *
  214. * @param {boolean} [crossDomain=false]
  215. * Whether tunnel requests will be cross-domain, and thus must use CORS
  216. * mechanisms and headers. By default, it is assumed that tunnel requests
  217. * will be made to the same domain.
  218. *
  219. * @param {object} [extraTunnelHeaders={}]
  220. * Key value pairs containing the header names and values of any additional
  221. * headers to be sent in tunnel requests. By default, no extra headers will
  222. * be added.
  223. */
  224. Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
  225. /**
  226. * Reference to this HTTP tunnel.
  227. *
  228. * @private
  229. * @type {!Guacamole.HTTPTunnel}
  230. */
  231. var tunnel = this;
  232. var TUNNEL_CONNECT = tunnelURL + "?connect";
  233. var TUNNEL_READ = tunnelURL + "?read:";
  234. var TUNNEL_WRITE = tunnelURL + "?write:";
  235. var POLLING_ENABLED = 1;
  236. var POLLING_DISABLED = 0;
  237. // Default to polling - will be turned off automatically if not needed
  238. var pollingMode = POLLING_ENABLED;
  239. var sendingMessages = false;
  240. var outputMessageBuffer = "";
  241. // If requests are expected to be cross-domain, the cookie that the HTTP
  242. // tunnel depends on will only be sent if withCredentials is true
  243. var withCredentials = !!crossDomain;
  244. /**
  245. * The current receive timeout ID, if any.
  246. *
  247. * @private
  248. * @type {number}
  249. */
  250. var receive_timeout = null;
  251. /**
  252. * The current connection stability timeout ID, if any.
  253. *
  254. * @private
  255. * @type {number}
  256. */
  257. var unstableTimeout = null;
  258. /**
  259. * The current connection stability test ping interval ID, if any. This
  260. * will only be set upon successful connection.
  261. *
  262. * @private
  263. * @type {number}
  264. */
  265. var pingInterval = null;
  266. /**
  267. * The number of milliseconds to wait between connection stability test
  268. * pings.
  269. *
  270. * @private
  271. * @constant
  272. * @type {!number}
  273. */
  274. var PING_FREQUENCY = 500;
  275. /**
  276. * Additional headers to be sent in tunnel requests. This dictionary can be
  277. * populated with key/value header pairs to pass information such as authentication
  278. * tokens, etc.
  279. *
  280. * @private
  281. * @type {!object}
  282. */
  283. var extraHeaders = extraTunnelHeaders || {};
  284. /**
  285. * The name of the HTTP header containing the session token specific to the
  286. * HTTP tunnel implementation.
  287. *
  288. * @private
  289. * @constant
  290. * @type {!string}
  291. */
  292. var TUNNEL_TOKEN_HEADER = 'Guacamole-Tunnel-Token';
  293. /**
  294. * The session token currently assigned to this HTTP tunnel. All distinct
  295. * HTTP tunnel connections will have their own dedicated session token.
  296. *
  297. * @private
  298. * @type {string}
  299. */
  300. var tunnelSessionToken = null;
  301. /**
  302. * Adds the configured additional headers to the given request.
  303. *
  304. * @private
  305. * @param {!XMLHttpRequest} request
  306. * The request where the configured extra headers will be added.
  307. *
  308. * @param {!object} headers
  309. * The headers to be added to the request.
  310. */
  311. function addExtraHeaders(request, headers) {
  312. for (var name in headers) {
  313. request.setRequestHeader(name, headers[name]);
  314. }
  315. }
  316. /**
  317. * Initiates a timeout which, if data is not received, causes the tunnel
  318. * to close with an error.
  319. *
  320. * @private
  321. */
  322. function reset_timeout() {
  323. // Get rid of old timeouts (if any)
  324. window.clearTimeout(receive_timeout);
  325. window.clearTimeout(unstableTimeout);
  326. // Clear unstable status
  327. if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
  328. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  329. // Set new timeout for tracking overall connection timeout
  330. receive_timeout = window.setTimeout(function () {
  331. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
  332. }, tunnel.receiveTimeout);
  333. // Set new timeout for tracking suspected connection instability
  334. unstableTimeout = window.setTimeout(function() {
  335. tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
  336. }, tunnel.unstableThreshold);
  337. }
  338. /**
  339. * Closes this tunnel, signaling the given status and corresponding
  340. * message, which will be sent to the onerror handler if the status is
  341. * an error status.
  342. *
  343. * @private
  344. * @param {!Guacamole.Status} status
  345. * The status causing the connection to close;
  346. */
  347. function close_tunnel(status) {
  348. // Get rid of old timeouts (if any)
  349. window.clearTimeout(receive_timeout);
  350. window.clearTimeout(unstableTimeout);
  351. // Cease connection test pings
  352. window.clearInterval(pingInterval);
  353. // Ignore if already closed
  354. if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
  355. return;
  356. // If connection closed abnormally, signal error.
  357. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) {
  358. // Ignore RESOURCE_NOT_FOUND if we've already connected, as that
  359. // only signals end-of-stream for the HTTP tunnel.
  360. if (tunnel.state === Guacamole.Tunnel.State.CONNECTING
  361. || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND)
  362. tunnel.onerror(status);
  363. }
  364. // Reset output message buffer
  365. sendingMessages = false;
  366. // Mark as closed
  367. tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  368. }
  369. this.sendMessage = function() {
  370. // Do not attempt to send messages if not connected
  371. if (!tunnel.isConnected())
  372. return;
  373. // Do not attempt to send empty messages
  374. if (arguments.length === 0)
  375. return;
  376. /**
  377. * Converts the given value to a length/string pair for use as an
  378. * element in a Guacamole instruction.
  379. *
  380. * @private
  381. * @param value
  382. * The value to convert.
  383. *
  384. * @return {!string}
  385. * The converted value.
  386. */
  387. function getElement(value) {
  388. var string = new String(value);
  389. return string.length + "." + string;
  390. }
  391. // Initialized message with first element
  392. var message = getElement(arguments[0]);
  393. // Append remaining elements
  394. for (var i=1; i<arguments.length; i++)
  395. message += "," + getElement(arguments[i]);
  396. // Final terminator
  397. message += ";";
  398. // Add message to buffer
  399. outputMessageBuffer += message;
  400. // Send if not currently sending
  401. if (!sendingMessages)
  402. sendPendingMessages();
  403. };
  404. function sendPendingMessages() {
  405. // Do not attempt to send messages if not connected
  406. if (!tunnel.isConnected())
  407. return;
  408. if (outputMessageBuffer.length > 0) {
  409. sendingMessages = true;
  410. var message_xmlhttprequest = new XMLHttpRequest();
  411. message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid);
  412. message_xmlhttprequest.withCredentials = withCredentials;
  413. addExtraHeaders(message_xmlhttprequest, extraHeaders);
  414. message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream");
  415. message_xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
  416. // Once response received, send next queued event.
  417. message_xmlhttprequest.onreadystatechange = function() {
  418. if (message_xmlhttprequest.readyState === 4) {
  419. reset_timeout();
  420. // If an error occurs during send, handle it
  421. if (message_xmlhttprequest.status !== 200)
  422. handleHTTPTunnelError(message_xmlhttprequest);
  423. // Otherwise, continue the send loop
  424. else
  425. sendPendingMessages();
  426. }
  427. };
  428. message_xmlhttprequest.send(outputMessageBuffer);
  429. outputMessageBuffer = ""; // Clear buffer
  430. }
  431. else
  432. sendingMessages = false;
  433. }
  434. function handleHTTPTunnelError(xmlhttprequest) {
  435. // Pull status code directly from headers provided by Guacamole
  436. var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
  437. if (code) {
  438. var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
  439. close_tunnel(new Guacamole.Status(code, message));
  440. }
  441. // Failing that, derive a Guacamole status code from the HTTP status
  442. // code provided by the browser
  443. else if (xmlhttprequest.status)
  444. close_tunnel(new Guacamole.Status(
  445. Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status),
  446. xmlhttprequest.statusText));
  447. // Otherwise, assume server is unreachable
  448. else
  449. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  450. }
  451. function handleResponse(xmlhttprequest) {
  452. var interval = null;
  453. var nextRequest = null;
  454. var dataUpdateEvents = 0;
  455. // The location of the last element's terminator
  456. var elementEnd = -1;
  457. // Where to start the next length search or the next element
  458. var startIndex = 0;
  459. // Parsed elements
  460. var elements = new Array();
  461. function parseResponse() {
  462. // Do not handle responses if not connected
  463. if (!tunnel.isConnected()) {
  464. // Clean up interval if polling
  465. if (interval !== null)
  466. clearInterval(interval);
  467. return;
  468. }
  469. // Do not parse response yet if not ready
  470. if (xmlhttprequest.readyState < 2) return;
  471. // Attempt to read status
  472. var status;
  473. try { status = xmlhttprequest.status; }
  474. // If status could not be read, assume successful.
  475. catch (e) { status = 200; }
  476. // Start next request as soon as possible IF request was successful
  477. if (!nextRequest && status === 200)
  478. nextRequest = makeRequest();
  479. // Parse stream when data is received and when complete.
  480. if (xmlhttprequest.readyState === 3 ||
  481. xmlhttprequest.readyState === 4) {
  482. reset_timeout();
  483. // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
  484. if (pollingMode === POLLING_ENABLED) {
  485. if (xmlhttprequest.readyState === 3 && !interval)
  486. interval = setInterval(parseResponse, 30);
  487. else if (xmlhttprequest.readyState === 4 && interval)
  488. clearInterval(interval);
  489. }
  490. // If canceled, stop transfer
  491. if (xmlhttprequest.status === 0) {
  492. tunnel.disconnect();
  493. return;
  494. }
  495. // Halt on error during request
  496. else if (xmlhttprequest.status !== 200) {
  497. handleHTTPTunnelError(xmlhttprequest);
  498. return;
  499. }
  500. // Attempt to read in-progress data
  501. var current;
  502. try { current = xmlhttprequest.responseText; }
  503. // Do not attempt to parse if data could not be read
  504. catch (e) { return; }
  505. // While search is within currently received data
  506. while (elementEnd < current.length) {
  507. // If we are waiting for element data
  508. if (elementEnd >= startIndex) {
  509. // We now have enough data for the element. Parse.
  510. var element = current.substring(startIndex, elementEnd);
  511. var terminator = current.substring(elementEnd, elementEnd+1);
  512. // Add element to array
  513. elements.push(element);
  514. // If last element, handle instruction
  515. if (terminator === ";") {
  516. // Get opcode
  517. var opcode = elements.shift();
  518. // Call instruction handler.
  519. if (tunnel.oninstruction)
  520. tunnel.oninstruction(opcode, elements);
  521. // Clear elements
  522. elements.length = 0;
  523. }
  524. // Start searching for length at character after
  525. // element terminator
  526. startIndex = elementEnd + 1;
  527. }
  528. // Search for end of length
  529. var lengthEnd = current.indexOf(".", startIndex);
  530. if (lengthEnd !== -1) {
  531. // Parse length
  532. var length = parseInt(current.substring(elementEnd+1, lengthEnd));
  533. // If we're done parsing, handle the next response.
  534. if (length === 0) {
  535. // Clean up interval if polling
  536. if (interval)
  537. clearInterval(interval);
  538. // Clean up object
  539. xmlhttprequest.onreadystatechange = null;
  540. xmlhttprequest.abort();
  541. // Start handling next request
  542. if (nextRequest)
  543. handleResponse(nextRequest);
  544. // Done parsing
  545. break;
  546. }
  547. // Calculate start of element
  548. startIndex = lengthEnd + 1;
  549. // Calculate location of element terminator
  550. elementEnd = startIndex + length;
  551. }
  552. // If no period yet, continue search when more data
  553. // is received
  554. else {
  555. startIndex = current.length;
  556. break;
  557. }
  558. } // end parse loop
  559. }
  560. }
  561. // If response polling enabled, attempt to detect if still
  562. // necessary (via wrapping parseResponse())
  563. if (pollingMode === POLLING_ENABLED) {
  564. xmlhttprequest.onreadystatechange = function() {
  565. // If we receive two or more readyState==3 events,
  566. // there is no need to poll.
  567. if (xmlhttprequest.readyState === 3) {
  568. dataUpdateEvents++;
  569. if (dataUpdateEvents >= 2) {
  570. pollingMode = POLLING_DISABLED;
  571. xmlhttprequest.onreadystatechange = parseResponse;
  572. }
  573. }
  574. parseResponse();
  575. };
  576. }
  577. // Otherwise, just parse
  578. else
  579. xmlhttprequest.onreadystatechange = parseResponse;
  580. parseResponse();
  581. }
  582. /**
  583. * Arbitrary integer, unique for each tunnel read request.
  584. * @private
  585. */
  586. var request_id = 0;
  587. function makeRequest() {
  588. // Make request, increment request ID
  589. var xmlhttprequest = new XMLHttpRequest();
  590. xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++));
  591. xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
  592. xmlhttprequest.withCredentials = withCredentials;
  593. addExtraHeaders(xmlhttprequest, extraHeaders);
  594. xmlhttprequest.send(null);
  595. return xmlhttprequest;
  596. }
  597. this.connect = function(data) {
  598. // Start waiting for connect
  599. reset_timeout();
  600. // Mark the tunnel as connecting
  601. tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  602. // Start tunnel and connect
  603. var connect_xmlhttprequest = new XMLHttpRequest();
  604. connect_xmlhttprequest.onreadystatechange = function() {
  605. if (connect_xmlhttprequest.readyState !== 4)
  606. return;
  607. // If failure, throw error
  608. if (connect_xmlhttprequest.status !== 200) {
  609. handleHTTPTunnelError(connect_xmlhttprequest);
  610. return;
  611. }
  612. reset_timeout();
  613. // Get UUID and HTTP-specific tunnel session token from response
  614. tunnel.setUUID(connect_xmlhttprequest.responseText);
  615. tunnelSessionToken = connect_xmlhttprequest.getResponseHeader(TUNNEL_TOKEN_HEADER);
  616. // Fail connect attempt if token is not successfully assigned
  617. if (!tunnelSessionToken) {
  618. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  619. return;
  620. }
  621. // Mark as open
  622. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  623. // Ping tunnel endpoint regularly to test connection stability
  624. pingInterval = setInterval(function sendPing() {
  625. tunnel.sendMessage("nop");
  626. }, PING_FREQUENCY);
  627. // Start reading data
  628. handleResponse(makeRequest());
  629. };
  630. connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
  631. connect_xmlhttprequest.withCredentials = withCredentials;
  632. addExtraHeaders(connect_xmlhttprequest, extraHeaders);
  633. connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
  634. connect_xmlhttprequest.send(data);
  635. };
  636. this.disconnect = function() {
  637. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
  638. };
  639. };
  640. Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
  641. /**
  642. * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
  643. *
  644. * @constructor
  645. * @augments Guacamole.Tunnel
  646. * @param {!string} tunnelURL
  647. * The URL of the WebSocket tunneling service.
  648. */
  649. Guacamole.WebSocketTunnel = function(tunnelURL) {
  650. /**
  651. * Reference to this WebSocket tunnel.
  652. *
  653. * @private
  654. * @type {Guacamole.WebSocketTunnel}
  655. */
  656. var tunnel = this;
  657. /**
  658. * The WebSocket used by this tunnel.
  659. *
  660. * @private
  661. * @type {WebSocket}
  662. */
  663. var socket = null;
  664. /**
  665. * The current receive timeout ID, if any.
  666. *
  667. * @private
  668. * @type {number}
  669. */
  670. var receive_timeout = null;
  671. /**
  672. * The current connection stability timeout ID, if any.
  673. *
  674. * @private
  675. * @type {number}
  676. */
  677. var unstableTimeout = null;
  678. /**
  679. * The current connection stability test ping interval ID, if any. This
  680. * will only be set upon successful connection.
  681. *
  682. * @private
  683. * @type {number}
  684. */
  685. var pingInterval = null;
  686. /**
  687. * The WebSocket protocol corresponding to the protocol used for the current
  688. * location.
  689. *
  690. * @private
  691. * @type {!Object.<string, string>}
  692. */
  693. var ws_protocol = {
  694. "http:": "ws:",
  695. "https:": "wss:"
  696. };
  697. /**
  698. * The number of milliseconds to wait between connection stability test
  699. * pings.
  700. *
  701. * @private
  702. * @constant
  703. * @type {!number}
  704. */
  705. var PING_FREQUENCY = 500;
  706. // Transform current URL to WebSocket URL
  707. // If not already a websocket URL
  708. if ( tunnelURL.substring(0, 3) !== "ws:"
  709. && tunnelURL.substring(0, 4) !== "wss:") {
  710. var protocol = ws_protocol[window.location.protocol];
  711. // If absolute URL, convert to absolute WS URL
  712. if (tunnelURL.substring(0, 1) === "/")
  713. tunnelURL =
  714. protocol
  715. + "//" + window.location.host
  716. + tunnelURL;
  717. // Otherwise, construct absolute from relative URL
  718. else {
  719. // Get path from pathname
  720. var slash = window.location.pathname.lastIndexOf("/");
  721. var path = window.location.pathname.substring(0, slash + 1);
  722. // Construct absolute URL
  723. tunnelURL =
  724. protocol
  725. + "//" + window.location.host
  726. + path
  727. + tunnelURL;
  728. }
  729. }
  730. /**
  731. * Initiates a timeout which, if data is not received, causes the tunnel
  732. * to close with an error.
  733. *
  734. * @private
  735. */
  736. function reset_timeout() {
  737. // Get rid of old timeouts (if any)
  738. window.clearTimeout(receive_timeout);
  739. window.clearTimeout(unstableTimeout);
  740. // Clear unstable status
  741. if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
  742. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  743. // Set new timeout for tracking overall connection timeout
  744. receive_timeout = window.setTimeout(function () {
  745. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
  746. }, tunnel.receiveTimeout);
  747. // Set new timeout for tracking suspected connection instability
  748. unstableTimeout = window.setTimeout(function() {
  749. tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
  750. }, tunnel.unstableThreshold);
  751. }
  752. /**
  753. * Closes this tunnel, signaling the given status and corresponding
  754. * message, which will be sent to the onerror handler if the status is
  755. * an error status.
  756. *
  757. * @private
  758. * @param {!Guacamole.Status} status
  759. * The status causing the connection to close;
  760. */
  761. function close_tunnel(status) {
  762. // Get rid of old timeouts (if any)
  763. window.clearTimeout(receive_timeout);
  764. window.clearTimeout(unstableTimeout);
  765. // Cease connection test pings
  766. window.clearInterval(pingInterval);
  767. // Ignore if already closed
  768. if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
  769. return;
  770. // If connection closed abnormally, signal error.
  771. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
  772. tunnel.onerror(status);
  773. // Mark as closed
  774. tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  775. socket.close();
  776. }
  777. this.sendMessage = function(elements) {
  778. // Do not attempt to send messages if not connected
  779. if (!tunnel.isConnected())
  780. return;
  781. // Do not attempt to send empty messages
  782. if (arguments.length === 0)
  783. return;
  784. /**
  785. * Converts the given value to a length/string pair for use as an
  786. * element in a Guacamole instruction.
  787. *
  788. * @private
  789. * @param {*} value
  790. * The value to convert.
  791. *
  792. * @return {!string}
  793. * The converted value.
  794. */
  795. function getElement(value) {
  796. var string = new String(value);
  797. return string.length + "." + string;
  798. }
  799. // Initialized message with first element
  800. var message = getElement(arguments[0]);
  801. // Append remaining elements
  802. for (var i=1; i<arguments.length; i++)
  803. message += "," + getElement(arguments[i]);
  804. // Final terminator
  805. message += ";";
  806. socket.send(message);
  807. };
  808. this.connect = function(data) {
  809. reset_timeout();
  810. // Mark the tunnel as connecting
  811. tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  812. // Connect socket
  813. socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
  814. socket.onopen = function(event) {
  815. reset_timeout();
  816. // Ping tunnel endpoint regularly to test connection stability
  817. pingInterval = setInterval(function sendPing() {
  818. tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE,
  819. "ping", new Date().getTime());
  820. }, PING_FREQUENCY);
  821. };
  822. socket.onclose = function(event) {
  823. // Pull status code directly from closure reason provided by Guacamole
  824. if (event.reason)
  825. close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
  826. // Failing that, derive a Guacamole status code from the WebSocket
  827. // status code provided by the browser
  828. else if (event.code)
  829. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code)));
  830. // Otherwise, assume server is unreachable
  831. else
  832. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  833. };
  834. socket.onmessage = function(event) {
  835. reset_timeout();
  836. var message = event.data;
  837. var startIndex = 0;
  838. var elementEnd;
  839. var elements = [];
  840. do {
  841. // Search for end of length
  842. var lengthEnd = message.indexOf(".", startIndex);
  843. if (lengthEnd !== -1) {
  844. // Parse length
  845. var length = parseInt(message.substring(elementEnd+1, lengthEnd));
  846. // Calculate start of element
  847. startIndex = lengthEnd + 1;
  848. // Calculate location of element terminator
  849. elementEnd = startIndex + length;
  850. }
  851. // If no period, incomplete instruction.
  852. else
  853. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction."));
  854. // We now have enough data for the element. Parse.
  855. var element = message.substring(startIndex, elementEnd);
  856. var terminator = message.substring(elementEnd, elementEnd+1);
  857. // Add element to array
  858. elements.push(element);
  859. // If last element, handle instruction
  860. if (terminator === ";") {
  861. // Get opcode
  862. var opcode = elements.shift();
  863. // Update state and UUID when first instruction received
  864. if (tunnel.uuid === null) {
  865. // Associate tunnel UUID if received
  866. if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE)
  867. tunnel.setUUID(elements[0]);
  868. // Tunnel is now open and UUID is available
  869. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  870. }
  871. // Call instruction handler.
  872. if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
  873. tunnel.oninstruction(opcode, elements);
  874. // Clear elements
  875. elements.length = 0;
  876. }
  877. // Start searching for length at character after
  878. // element terminator
  879. startIndex = elementEnd + 1;
  880. } while (startIndex < message.length);
  881. };
  882. };
  883. this.disconnect = function() {
  884. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
  885. };
  886. };
  887. Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
  888. /**
  889. * Guacamole Tunnel which cycles between all specified tunnels until
  890. * no tunnels are left. Another tunnel is used if an error occurs but
  891. * no instructions have been received. If an instruction has been
  892. * received, or no tunnels remain, the error is passed directly out
  893. * through the onerror handler (if defined).
  894. *
  895. * @constructor
  896. * @augments Guacamole.Tunnel
  897. * @param {...Guacamole.Tunnel} tunnelChain
  898. * The tunnels to use, in order of priority.
  899. */
  900. Guacamole.ChainedTunnel = function(tunnelChain) {
  901. /**
  902. * Reference to this chained tunnel.
  903. * @private
  904. */
  905. var chained_tunnel = this;
  906. /**
  907. * Data passed in via connect(), to be used for
  908. * wrapped calls to other tunnels' connect() functions.
  909. * @private
  910. */
  911. var connect_data;
  912. /**
  913. * Array of all tunnels passed to this ChainedTunnel through the
  914. * constructor arguments.
  915. * @private
  916. */
  917. var tunnels = [];
  918. /**
  919. * The tunnel committed via commit_tunnel(), if any, or null if no tunnel
  920. * has yet been committed.
  921. *
  922. * @private
  923. * @type {Guacamole.Tunnel}
  924. */
  925. var committedTunnel = null;
  926. // Load all tunnels into array
  927. for (var i=0; i<arguments.length; i++)
  928. tunnels.push(arguments[i]);
  929. /**
  930. * Sets the current tunnel.
  931. *
  932. * @private
  933. * @param {!Guacamole.Tunnel} tunnel
  934. * The tunnel to set as the current tunnel.
  935. */
  936. function attach(tunnel) {
  937. // Set own functions to tunnel's functions
  938. chained_tunnel.disconnect = tunnel.disconnect;
  939. chained_tunnel.sendMessage = tunnel.sendMessage;
  940. /**
  941. * Fails the currently-attached tunnel, attaching a new tunnel if
  942. * possible.
  943. *
  944. * @private
  945. * @param {Guacamole.Status} [status]
  946. * An object representing the failure that occured in the
  947. * currently-attached tunnel, if known.
  948. *
  949. * @return {Guacamole.Tunnel}
  950. * The next tunnel, or null if there are no more tunnels to try or
  951. * if no more tunnels should be tried.
  952. */
  953. var failTunnel = function failTunnel(status) {
  954. // Do not attempt to continue using next tunnel on server timeout
  955. if (status && status.code === Guacamole.Status.Code.UPSTREAM_TIMEOUT) {
  956. tunnels = [];
  957. return null;
  958. }
  959. // Get next tunnel
  960. var next_tunnel = tunnels.shift();
  961. // If there IS a next tunnel, try using it.
  962. if (next_tunnel) {
  963. tunnel.onerror = null;
  964. tunnel.oninstruction = null;
  965. tunnel.onstatechange = null;
  966. attach(next_tunnel);
  967. }
  968. return next_tunnel;
  969. };
  970. /**
  971. * Use the current tunnel from this point forward. Do not try any more
  972. * tunnels, even if the current tunnel fails.
  973. *
  974. * @private
  975. */
  976. function commit_tunnel() {
  977. tunnel.onstatechange = chained_tunnel.onstatechange;
  978. tunnel.oninstruction = chained_tunnel.oninstruction;
  979. tunnel.onerror = chained_tunnel.onerror;
  980. tunnel.onuuid = chained_tunnel.onuuid;
  981. // Assign UUID if already known
  982. if (tunnel.uuid)
  983. chained_tunnel.setUUID(tunnel.uuid);
  984. committedTunnel = tunnel;
  985. }
  986. // Wrap own onstatechange within current tunnel
  987. tunnel.onstatechange = function(state) {
  988. switch (state) {
  989. // If open, use this tunnel from this point forward.
  990. case Guacamole.Tunnel.State.OPEN:
  991. commit_tunnel();
  992. if (chained_tunnel.onstatechange)
  993. chained_tunnel.onstatechange(state);
  994. break;
  995. // If closed, mark failure, attempt next tunnel
  996. case Guacamole.Tunnel.State.CLOSED:
  997. if (!failTunnel() && chained_tunnel.onstatechange)
  998. chained_tunnel.onstatechange(state);
  999. break;
  1000. }
  1001. };
  1002. // Wrap own oninstruction within current tunnel
  1003. tunnel.oninstruction = function(opcode, elements) {
  1004. // Accept current tunnel
  1005. commit_tunnel();
  1006. // Invoke handler
  1007. if (chained_tunnel.oninstruction)
  1008. chained_tunnel.oninstruction(opcode, elements);
  1009. };
  1010. // Attach next tunnel on error
  1011. tunnel.onerror = function(status) {
  1012. // Mark failure, attempt next tunnel
  1013. if (!failTunnel(status) && chained_tunnel.onerror)
  1014. chained_tunnel.onerror(status);
  1015. };
  1016. // Attempt connection
  1017. tunnel.connect(connect_data);
  1018. }
  1019. this.connect = function(data) {
  1020. // Remember connect data
  1021. connect_data = data;
  1022. // Get committed tunnel if exists or the first tunnel on the list
  1023. var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift();
  1024. // Attach first tunnel
  1025. if (next_tunnel)
  1026. attach(next_tunnel);
  1027. // If there IS no first tunnel, error
  1028. else if (chained_tunnel.onerror)
  1029. chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try.");
  1030. };
  1031. };
  1032. Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();
  1033. /**
  1034. * Guacamole Tunnel which replays a Guacamole protocol dump from a static file
  1035. * received via HTTP. Instructions within the file are parsed and handled as
  1036. * quickly as possible, while the file is being downloaded.
  1037. *
  1038. * @constructor
  1039. * @augments Guacamole.Tunnel
  1040. * @param {!string} url
  1041. * The URL of a Guacamole protocol dump.
  1042. *
  1043. * @param {boolean} [crossDomain=false]
  1044. * Whether tunnel requests will be cross-domain, and thus must use CORS
  1045. * mechanisms and headers. By default, it is assumed that tunnel requests
  1046. * will be made to the same domain.
  1047. *
  1048. * @param {object} [extraTunnelHeaders={}]
  1049. * Key value pairs containing the header names and values of any additional
  1050. * headers to be sent in tunnel requests. By default, no extra headers will
  1051. * be added.
  1052. */
  1053. Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTunnelHeaders) {
  1054. /**
  1055. * Reference to this Guacamole.StaticHTTPTunnel.
  1056. *
  1057. * @private
  1058. */
  1059. var tunnel = this;
  1060. /**
  1061. * AbortController instance which allows the current, in-progress HTTP
  1062. * request to be aborted. If no request is currently in progress, this will
  1063. * be null.
  1064. *
  1065. * @private
  1066. * @type {AbortController}
  1067. */
  1068. var abortController = null;
  1069. /**
  1070. * Additional headers to be sent in tunnel requests. This dictionary can be
  1071. * populated with key/value header pairs to pass information such as authentication
  1072. * tokens, etc.
  1073. *
  1074. * @private
  1075. * @type {!object}
  1076. */
  1077. var extraHeaders = extraTunnelHeaders || {};
  1078. /**
  1079. * The number of bytes in the file being downloaded, or null if this is not
  1080. * known.
  1081. *
  1082. * @type {number}
  1083. */
  1084. this.size = null;
  1085. this.sendMessage = function sendMessage(elements) {
  1086. // Do nothing
  1087. };
  1088. this.connect = function connect(data) {
  1089. // Ensure any existing connection is killed
  1090. tunnel.disconnect();
  1091. // Connection is now starting
  1092. tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  1093. // Create Guacamole protocol and UTF-8 parsers specifically for this
  1094. // connection
  1095. var parser = new Guacamole.Parser();
  1096. var utf8Parser = new Guacamole.UTF8Parser();
  1097. // Invoke tunnel's oninstruction handler for each parsed instruction
  1098. parser.oninstruction = function instructionReceived(opcode, args) {
  1099. if (tunnel.oninstruction)
  1100. tunnel.oninstruction(opcode, args);
  1101. };
  1102. // Allow new request to be aborted
  1103. abortController = new AbortController();
  1104. // Stream using the Fetch API
  1105. fetch(url, {
  1106. headers : extraHeaders,
  1107. credentials : crossDomain ? 'include' : 'same-origin',
  1108. signal : abortController.signal
  1109. })
  1110. .then(function gotResponse(response) {
  1111. // Reset state and close upon error
  1112. if (!response.ok) {
  1113. if (tunnel.onerror)
  1114. tunnel.onerror(new Guacamole.Status(
  1115. Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText));
  1116. tunnel.disconnect();
  1117. return;
  1118. }
  1119. // Report overall size of stream in bytes, if known
  1120. tunnel.size = response.headers.get('Content-Length');
  1121. // Connection is open
  1122. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  1123. var reader = response.body.getReader();
  1124. var processReceivedText = function processReceivedText(result) {
  1125. // Clean up and close when done
  1126. if (result.done) {
  1127. tunnel.disconnect();
  1128. return;
  1129. }
  1130. // Parse only the portion of data which is newly received
  1131. parser.receive(utf8Parser.decode(result.value));
  1132. // Continue parsing when next chunk is received
  1133. reader.read().then(processReceivedText);
  1134. };
  1135. // Schedule parse of first chunk
  1136. reader.read().then(processReceivedText);
  1137. });
  1138. };
  1139. this.disconnect = function disconnect() {
  1140. // Abort any in-progress request
  1141. if (abortController) {
  1142. abortController.abort();
  1143. abortController = null;
  1144. }
  1145. // Connection is now closed
  1146. tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  1147. };
  1148. };
  1149. Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel();