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