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