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. * Resets the state of timers tracking network activity and stability. If
  318. * those timers are not yet started, invoking this function starts them.
  319. * This function should be invoked when the tunnel is established and every
  320. * time there is network activity on the tunnel, such that the timers can
  321. * safely assume the network and/or server are not responding if this
  322. * function has not been invoked for a significant period of time.
  323. *
  324. * @private
  325. */
  326. var resetTimers = function resetTimers() {
  327. // Get rid of old timeouts (if any)
  328. window.clearTimeout(receive_timeout);
  329. window.clearTimeout(unstableTimeout);
  330. // Clear unstable status
  331. if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
  332. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  333. // Set new timeout for tracking overall connection timeout
  334. receive_timeout = window.setTimeout(function () {
  335. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
  336. }, tunnel.receiveTimeout);
  337. // Set new timeout for tracking suspected connection instability
  338. unstableTimeout = window.setTimeout(function() {
  339. tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
  340. }, tunnel.unstableThreshold);
  341. };
  342. /**
  343. * Closes this tunnel, signaling the given status and corresponding
  344. * message, which will be sent to the onerror handler if the status is
  345. * an error status.
  346. *
  347. * @private
  348. * @param {!Guacamole.Status} status
  349. * The status causing the connection to close;
  350. */
  351. function close_tunnel(status) {
  352. // Get rid of old timeouts (if any)
  353. window.clearTimeout(receive_timeout);
  354. window.clearTimeout(unstableTimeout);
  355. // Cease connection test pings
  356. window.clearInterval(pingInterval);
  357. // Ignore if already closed
  358. if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
  359. return;
  360. // If connection closed abnormally, signal error.
  361. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) {
  362. // Ignore RESOURCE_NOT_FOUND if we've already connected, as that
  363. // only signals end-of-stream for the HTTP tunnel.
  364. if (tunnel.state === Guacamole.Tunnel.State.CONNECTING
  365. || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND)
  366. tunnel.onerror(status);
  367. }
  368. // Reset output message buffer
  369. sendingMessages = false;
  370. // Mark as closed
  371. tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  372. }
  373. this.sendMessage = function() {
  374. // Do not attempt to send messages if not connected
  375. if (!tunnel.isConnected())
  376. return;
  377. // Do not attempt to send empty messages
  378. if (!arguments.length)
  379. return;
  380. // Add message to buffer
  381. outputMessageBuffer += Guacamole.Parser.toInstruction(arguments);
  382. // Send if not currently sending
  383. if (!sendingMessages)
  384. sendPendingMessages();
  385. };
  386. function sendPendingMessages() {
  387. // Do not attempt to send messages if not connected
  388. if (!tunnel.isConnected())
  389. return;
  390. if (outputMessageBuffer.length > 0) {
  391. sendingMessages = true;
  392. var message_xmlhttprequest = new XMLHttpRequest();
  393. message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid);
  394. message_xmlhttprequest.withCredentials = withCredentials;
  395. addExtraHeaders(message_xmlhttprequest, extraHeaders);
  396. message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream");
  397. message_xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
  398. // Once response received, send next queued event.
  399. message_xmlhttprequest.onreadystatechange = function() {
  400. if (message_xmlhttprequest.readyState === 4) {
  401. resetTimers();
  402. // If an error occurs during send, handle it
  403. if (message_xmlhttprequest.status !== 200)
  404. handleHTTPTunnelError(message_xmlhttprequest);
  405. // Otherwise, continue the send loop
  406. else
  407. sendPendingMessages();
  408. }
  409. };
  410. message_xmlhttprequest.send(outputMessageBuffer);
  411. outputMessageBuffer = ""; // Clear buffer
  412. }
  413. else
  414. sendingMessages = false;
  415. }
  416. function handleHTTPTunnelError(xmlhttprequest) {
  417. // Pull status code directly from headers provided by Guacamole
  418. var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
  419. if (code) {
  420. var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
  421. close_tunnel(new Guacamole.Status(code, message));
  422. }
  423. // Failing that, derive a Guacamole status code from the HTTP status
  424. // code provided by the browser
  425. else if (xmlhttprequest.status)
  426. close_tunnel(new Guacamole.Status(
  427. Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status),
  428. xmlhttprequest.statusText));
  429. // Otherwise, assume server is unreachable
  430. else
  431. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  432. }
  433. function handleResponse(xmlhttprequest) {
  434. var interval = null;
  435. var nextRequest = null;
  436. var dataUpdateEvents = 0;
  437. var parser = new Guacamole.Parser();
  438. parser.oninstruction = function instructionReceived(opcode, args) {
  439. // Switch to next request if end-of-stream is signalled
  440. if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && args.length === 0) {
  441. // Reset parser state by simply switching to an entirely new
  442. // parser
  443. parser = new Guacamole.Parser();
  444. parser.oninstruction = instructionReceived;
  445. // Clean up interval if polling
  446. if (interval)
  447. clearInterval(interval);
  448. // Clean up object
  449. xmlhttprequest.onreadystatechange = null;
  450. xmlhttprequest.abort();
  451. // Start handling next request
  452. if (nextRequest)
  453. handleResponse(nextRequest);
  454. }
  455. // Call instruction handler.
  456. else if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
  457. tunnel.oninstruction(opcode, args);
  458. };
  459. function parseResponse() {
  460. // Do not handle responses if not connected
  461. if (!tunnel.isConnected()) {
  462. // Clean up interval if polling
  463. if (interval !== null)
  464. clearInterval(interval);
  465. return;
  466. }
  467. // Do not parse response yet if not ready
  468. if (xmlhttprequest.readyState < 2) return;
  469. // Attempt to read status
  470. var status;
  471. try { status = xmlhttprequest.status; }
  472. // If status could not be read, assume successful.
  473. catch (e) { status = 200; }
  474. // Start next request as soon as possible IF request was successful
  475. if (!nextRequest && status === 200)
  476. nextRequest = makeRequest();
  477. // Parse stream when data is received and when complete.
  478. if (xmlhttprequest.readyState === 3 ||
  479. xmlhttprequest.readyState === 4) {
  480. resetTimers();
  481. // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
  482. if (pollingMode === POLLING_ENABLED) {
  483. if (xmlhttprequest.readyState === 3 && !interval)
  484. interval = setInterval(parseResponse, 30);
  485. else if (xmlhttprequest.readyState === 4 && interval)
  486. clearInterval(interval);
  487. }
  488. // If canceled, stop transfer
  489. if (xmlhttprequest.status === 0) {
  490. tunnel.disconnect();
  491. return;
  492. }
  493. // Halt on error during request
  494. else if (xmlhttprequest.status !== 200) {
  495. handleHTTPTunnelError(xmlhttprequest);
  496. return;
  497. }
  498. // Attempt to read in-progress data
  499. var current;
  500. try { current = xmlhttprequest.responseText; }
  501. // Do not attempt to parse if data could not be read
  502. catch (e) { return; }
  503. try {
  504. parser.receive(current, true);
  505. }
  506. catch (e) {
  507. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, e.message));
  508. return;
  509. }
  510. }
  511. }
  512. // If response polling enabled, attempt to detect if still
  513. // necessary (via wrapping parseResponse())
  514. if (pollingMode === POLLING_ENABLED) {
  515. xmlhttprequest.onreadystatechange = function() {
  516. // If we receive two or more readyState==3 events,
  517. // there is no need to poll.
  518. if (xmlhttprequest.readyState === 3) {
  519. dataUpdateEvents++;
  520. if (dataUpdateEvents >= 2) {
  521. pollingMode = POLLING_DISABLED;
  522. xmlhttprequest.onreadystatechange = parseResponse;
  523. }
  524. }
  525. parseResponse();
  526. };
  527. }
  528. // Otherwise, just parse
  529. else
  530. xmlhttprequest.onreadystatechange = parseResponse;
  531. parseResponse();
  532. }
  533. /**
  534. * Arbitrary integer, unique for each tunnel read request.
  535. * @private
  536. */
  537. var request_id = 0;
  538. function makeRequest() {
  539. // Make request, increment request ID
  540. var xmlhttprequest = new XMLHttpRequest();
  541. xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++));
  542. xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
  543. xmlhttprequest.withCredentials = withCredentials;
  544. addExtraHeaders(xmlhttprequest, extraHeaders);
  545. xmlhttprequest.send(null);
  546. return xmlhttprequest;
  547. }
  548. this.connect = function(data) {
  549. // Start waiting for connect
  550. resetTimers();
  551. // Mark the tunnel as connecting
  552. tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  553. // Start tunnel and connect
  554. var connect_xmlhttprequest = new XMLHttpRequest();
  555. connect_xmlhttprequest.onreadystatechange = function() {
  556. if (connect_xmlhttprequest.readyState !== 4)
  557. return;
  558. // If failure, throw error
  559. if (connect_xmlhttprequest.status !== 200) {
  560. handleHTTPTunnelError(connect_xmlhttprequest);
  561. return;
  562. }
  563. resetTimers();
  564. // Get UUID and HTTP-specific tunnel session token from response
  565. tunnel.setUUID(connect_xmlhttprequest.responseText);
  566. tunnelSessionToken = connect_xmlhttprequest.getResponseHeader(TUNNEL_TOKEN_HEADER);
  567. // Fail connect attempt if token is not successfully assigned
  568. if (!tunnelSessionToken) {
  569. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  570. return;
  571. }
  572. // Mark as open
  573. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  574. // Ping tunnel endpoint regularly to test connection stability
  575. pingInterval = setInterval(function sendPing() {
  576. tunnel.sendMessage("nop");
  577. }, PING_FREQUENCY);
  578. // Start reading data
  579. handleResponse(makeRequest());
  580. };
  581. connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
  582. connect_xmlhttprequest.withCredentials = withCredentials;
  583. addExtraHeaders(connect_xmlhttprequest, extraHeaders);
  584. connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
  585. connect_xmlhttprequest.send(data);
  586. };
  587. this.disconnect = function() {
  588. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
  589. };
  590. };
  591. Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
  592. /**
  593. * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
  594. *
  595. * @constructor
  596. * @augments Guacamole.Tunnel
  597. * @param {!string} tunnelURL
  598. * The URL of the WebSocket tunneling service.
  599. */
  600. Guacamole.WebSocketTunnel = function(tunnelURL) {
  601. /**
  602. * Reference to this WebSocket tunnel.
  603. *
  604. * @private
  605. * @type {Guacamole.WebSocketTunnel}
  606. */
  607. var tunnel = this;
  608. /**
  609. * The parser that this tunnel will use to parse received Guacamole
  610. * instructions. The parser is created when the tunnel is (re-)connected.
  611. * Initially, this will be null.
  612. *
  613. * @private
  614. * @type {Guacamole.Parser}
  615. */
  616. var parser = null;
  617. /**
  618. * The WebSocket used by this tunnel.
  619. *
  620. * @private
  621. * @type {WebSocket}
  622. */
  623. var socket = null;
  624. /**
  625. * The current receive timeout ID, if any.
  626. *
  627. * @private
  628. * @type {number}
  629. */
  630. var receive_timeout = null;
  631. /**
  632. * The current connection stability timeout ID, if any.
  633. *
  634. * @private
  635. * @type {number}
  636. */
  637. var unstableTimeout = null;
  638. /**
  639. * The current connection stability test ping timeout ID, if any. This
  640. * will only be set upon successful connection.
  641. *
  642. * @private
  643. * @type {number}
  644. */
  645. var pingTimeout = null;
  646. /**
  647. * The WebSocket protocol corresponding to the protocol used for the current
  648. * location.
  649. *
  650. * @private
  651. * @type {!Object.<string, string>}
  652. */
  653. var ws_protocol = {
  654. "http:": "ws:",
  655. "https:": "wss:"
  656. };
  657. /**
  658. * The number of milliseconds to wait between connection stability test
  659. * pings.
  660. *
  661. * @private
  662. * @constant
  663. * @type {!number}
  664. */
  665. var PING_FREQUENCY = 500;
  666. /**
  667. * The timestamp of the point in time that the last connection stability
  668. * test ping was sent, in milliseconds elapsed since midnight of January 1,
  669. * 1970 UTC.
  670. *
  671. * @private
  672. * @type {!number}
  673. */
  674. var lastSentPing = 0;
  675. // Transform current URL to WebSocket URL
  676. // If not already a websocket URL
  677. if ( tunnelURL.substring(0, 3) !== "ws:"
  678. && tunnelURL.substring(0, 4) !== "wss:") {
  679. var protocol = ws_protocol[window.location.protocol];
  680. // If absolute URL, convert to absolute WS URL
  681. if (tunnelURL.substring(0, 1) === "/")
  682. tunnelURL =
  683. protocol
  684. + "//" + window.location.host
  685. + tunnelURL;
  686. // Otherwise, construct absolute from relative URL
  687. else {
  688. // Get path from pathname
  689. var slash = window.location.pathname.lastIndexOf("/");
  690. var path = window.location.pathname.substring(0, slash + 1);
  691. // Construct absolute URL
  692. tunnelURL =
  693. protocol
  694. + "//" + window.location.host
  695. + path
  696. + tunnelURL;
  697. }
  698. }
  699. /**
  700. * Sends an internal "ping" instruction to the Guacamole WebSocket
  701. * endpoint, verifying network connection stability. If the network is
  702. * stable, the Guacamole server will receive this instruction and respond
  703. * with an identical ping.
  704. *
  705. * @private
  706. */
  707. var sendPing = function sendPing() {
  708. var currentTime = new Date().getTime();
  709. tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime);
  710. lastSentPing = currentTime;
  711. };
  712. /**
  713. * Resets the state of timers tracking network activity and stability. If
  714. * those timers are not yet started, invoking this function starts them.
  715. * This function should be invoked when the tunnel is established and every
  716. * time there is network activity on the tunnel, such that the timers can
  717. * safely assume the network and/or server are not responding if this
  718. * function has not been invoked for a significant period of time.
  719. *
  720. * @private
  721. */
  722. var resetTimers = function resetTimers() {
  723. // Get rid of old timeouts (if any)
  724. window.clearTimeout(receive_timeout);
  725. window.clearTimeout(unstableTimeout);
  726. window.clearTimeout(pingTimeout);
  727. // Clear unstable status
  728. if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
  729. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  730. // Set new timeout for tracking overall connection timeout
  731. receive_timeout = window.setTimeout(function () {
  732. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
  733. }, tunnel.receiveTimeout);
  734. // Set new timeout for tracking suspected connection instability
  735. unstableTimeout = window.setTimeout(function() {
  736. tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
  737. }, tunnel.unstableThreshold);
  738. var currentTime = new Date().getTime();
  739. var pingDelay = Math.max(lastSentPing + PING_FREQUENCY - currentTime, 0);
  740. // Ping tunnel endpoint regularly to test connection stability, sending
  741. // the ping immediately if enough time has already elapsed
  742. if (pingDelay > 0)
  743. pingTimeout = window.setTimeout(sendPing, pingDelay);
  744. else
  745. sendPing();
  746. };
  747. /**
  748. * Closes this tunnel, signaling the given status and corresponding
  749. * message, which will be sent to the onerror handler if the status is
  750. * an error status.
  751. *
  752. * @private
  753. * @param {!Guacamole.Status} status
  754. * The status causing the connection to close;
  755. */
  756. function close_tunnel(status) {
  757. // Get rid of old timeouts (if any)
  758. window.clearTimeout(receive_timeout);
  759. window.clearTimeout(unstableTimeout);
  760. window.clearTimeout(pingTimeout);
  761. // Ignore if already closed
  762. if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
  763. return;
  764. // If connection closed abnormally, signal error.
  765. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
  766. tunnel.onerror(status);
  767. // Mark as closed
  768. tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  769. socket.close();
  770. }
  771. this.sendMessage = function(elements) {
  772. // Do not attempt to send messages if not connected
  773. if (!tunnel.isConnected())
  774. return;
  775. // Do not attempt to send empty messages
  776. if (!arguments.length)
  777. return;
  778. socket.send(Guacamole.Parser.toInstruction(arguments));
  779. };
  780. this.connect = function(data) {
  781. resetTimers();
  782. // Mark the tunnel as connecting
  783. tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  784. parser = new Guacamole.Parser();
  785. parser.oninstruction = function instructionReceived(opcode, args) {
  786. // Update state and UUID when first instruction received
  787. if (tunnel.uuid === null) {
  788. // Associate tunnel UUID if received
  789. if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && args.length === 1)
  790. tunnel.setUUID(args[0]);
  791. // Tunnel is now open and UUID is available
  792. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  793. }
  794. // Call instruction handler.
  795. if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
  796. tunnel.oninstruction(opcode, args);
  797. };
  798. // Connect socket
  799. socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
  800. socket.onopen = function(event) {
  801. resetTimers();
  802. };
  803. socket.onclose = function(event) {
  804. // Pull status code directly from closure reason provided by Guacamole
  805. if (event.reason)
  806. close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
  807. // Failing that, derive a Guacamole status code from the WebSocket
  808. // status code provided by the browser
  809. else if (event.code)
  810. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code)));
  811. // Otherwise, assume server is unreachable
  812. else
  813. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  814. };
  815. socket.onmessage = function(event) {
  816. resetTimers();
  817. try {
  818. parser.receive(event.data);
  819. }
  820. catch (e) {
  821. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, e.message));
  822. }
  823. };
  824. };
  825. this.disconnect = function() {
  826. close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
  827. };
  828. };
  829. Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
  830. /**
  831. * Guacamole Tunnel which cycles between all specified tunnels until
  832. * no tunnels are left. Another tunnel is used if an error occurs but
  833. * no instructions have been received. If an instruction has been
  834. * received, or no tunnels remain, the error is passed directly out
  835. * through the onerror handler (if defined).
  836. *
  837. * @constructor
  838. * @augments Guacamole.Tunnel
  839. * @param {...Guacamole.Tunnel} tunnelChain
  840. * The tunnels to use, in order of priority.
  841. */
  842. Guacamole.ChainedTunnel = function(tunnelChain) {
  843. /**
  844. * Reference to this chained tunnel.
  845. * @private
  846. */
  847. var chained_tunnel = this;
  848. /**
  849. * Data passed in via connect(), to be used for
  850. * wrapped calls to other tunnels' connect() functions.
  851. * @private
  852. */
  853. var connect_data;
  854. /**
  855. * Array of all tunnels passed to this ChainedTunnel through the
  856. * constructor arguments.
  857. * @private
  858. */
  859. var tunnels = [];
  860. /**
  861. * The tunnel committed via commit_tunnel(), if any, or null if no tunnel
  862. * has yet been committed.
  863. *
  864. * @private
  865. * @type {Guacamole.Tunnel}
  866. */
  867. var committedTunnel = null;
  868. // Load all tunnels into array
  869. for (var i=0; i<arguments.length; i++)
  870. tunnels.push(arguments[i]);
  871. /**
  872. * Sets the current tunnel.
  873. *
  874. * @private
  875. * @param {!Guacamole.Tunnel} tunnel
  876. * The tunnel to set as the current tunnel.
  877. */
  878. function attach(tunnel) {
  879. // Set own functions to tunnel's functions
  880. chained_tunnel.disconnect = tunnel.disconnect;
  881. chained_tunnel.sendMessage = tunnel.sendMessage;
  882. /**
  883. * Fails the currently-attached tunnel, attaching a new tunnel if
  884. * possible.
  885. *
  886. * @private
  887. * @param {Guacamole.Status} [status]
  888. * An object representing the failure that occured in the
  889. * currently-attached tunnel, if known.
  890. *
  891. * @return {Guacamole.Tunnel}
  892. * The next tunnel, or null if there are no more tunnels to try or
  893. * if no more tunnels should be tried.
  894. */
  895. var failTunnel = function failTunnel(status) {
  896. // Do not attempt to continue using next tunnel on server timeout
  897. if (status && status.code === Guacamole.Status.Code.UPSTREAM_TIMEOUT) {
  898. tunnels = [];
  899. return null;
  900. }
  901. // Get next tunnel
  902. var next_tunnel = tunnels.shift();
  903. // If there IS a next tunnel, try using it.
  904. if (next_tunnel) {
  905. tunnel.onerror = null;
  906. tunnel.oninstruction = null;
  907. tunnel.onstatechange = null;
  908. attach(next_tunnel);
  909. }
  910. return next_tunnel;
  911. };
  912. /**
  913. * Use the current tunnel from this point forward. Do not try any more
  914. * tunnels, even if the current tunnel fails.
  915. *
  916. * @private
  917. */
  918. function commit_tunnel() {
  919. tunnel.onstatechange = chained_tunnel.onstatechange;
  920. tunnel.oninstruction = chained_tunnel.oninstruction;
  921. tunnel.onerror = chained_tunnel.onerror;
  922. // Assign UUID if already known
  923. if (tunnel.uuid)
  924. chained_tunnel.setUUID(tunnel.uuid);
  925. // Assign any future received UUIDs such that they are
  926. // accessible from the main uuid property of the chained tunnel
  927. tunnel.onuuid = function uuidReceived(uuid) {
  928. chained_tunnel.setUUID(uuid);
  929. };
  930. committedTunnel = tunnel;
  931. }
  932. // Wrap own onstatechange within current tunnel
  933. tunnel.onstatechange = function(state) {
  934. switch (state) {
  935. // If open, use this tunnel from this point forward.
  936. case Guacamole.Tunnel.State.OPEN:
  937. commit_tunnel();
  938. if (chained_tunnel.onstatechange)
  939. chained_tunnel.onstatechange(state);
  940. break;
  941. // If closed, mark failure, attempt next tunnel
  942. case Guacamole.Tunnel.State.CLOSED:
  943. if (!failTunnel() && chained_tunnel.onstatechange)
  944. chained_tunnel.onstatechange(state);
  945. break;
  946. }
  947. };
  948. // Wrap own oninstruction within current tunnel
  949. tunnel.oninstruction = function(opcode, elements) {
  950. // Accept current tunnel
  951. commit_tunnel();
  952. // Invoke handler
  953. if (chained_tunnel.oninstruction)
  954. chained_tunnel.oninstruction(opcode, elements);
  955. };
  956. // Attach next tunnel on error
  957. tunnel.onerror = function(status) {
  958. // Mark failure, attempt next tunnel
  959. if (!failTunnel(status) && chained_tunnel.onerror)
  960. chained_tunnel.onerror(status);
  961. };
  962. // Attempt connection
  963. tunnel.connect(connect_data);
  964. }
  965. this.connect = function(data) {
  966. // Remember connect data
  967. connect_data = data;
  968. // Get committed tunnel if exists or the first tunnel on the list
  969. var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift();
  970. // Attach first tunnel
  971. if (next_tunnel)
  972. attach(next_tunnel);
  973. // If there IS no first tunnel, error
  974. else if (chained_tunnel.onerror)
  975. chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try.");
  976. };
  977. };
  978. Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();
  979. /**
  980. * Guacamole Tunnel which replays a Guacamole protocol dump from a static file
  981. * received via HTTP. Instructions within the file are parsed and handled as
  982. * quickly as possible, while the file is being downloaded.
  983. *
  984. * @constructor
  985. * @augments Guacamole.Tunnel
  986. * @param {!string} url
  987. * The URL of a Guacamole protocol dump.
  988. *
  989. * @param {boolean} [crossDomain=false]
  990. * Whether tunnel requests will be cross-domain, and thus must use CORS
  991. * mechanisms and headers. By default, it is assumed that tunnel requests
  992. * will be made to the same domain.
  993. *
  994. * @param {object} [extraTunnelHeaders={}]
  995. * Key value pairs containing the header names and values of any additional
  996. * headers to be sent in tunnel requests. By default, no extra headers will
  997. * be added.
  998. */
  999. Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTunnelHeaders) {
  1000. /**
  1001. * Reference to this Guacamole.StaticHTTPTunnel.
  1002. *
  1003. * @private
  1004. */
  1005. var tunnel = this;
  1006. /**
  1007. * AbortController instance which allows the current, in-progress HTTP
  1008. * request to be aborted. If no request is currently in progress, this will
  1009. * be null.
  1010. *
  1011. * @private
  1012. * @type {AbortController}
  1013. */
  1014. var abortController = null;
  1015. /**
  1016. * Additional headers to be sent in tunnel requests. This dictionary can be
  1017. * populated with key/value header pairs to pass information such as authentication
  1018. * tokens, etc.
  1019. *
  1020. * @private
  1021. * @type {!object}
  1022. */
  1023. var extraHeaders = extraTunnelHeaders || {};
  1024. /**
  1025. * The number of bytes in the file being downloaded, or null if this is not
  1026. * known.
  1027. *
  1028. * @type {number}
  1029. */
  1030. this.size = null;
  1031. this.sendMessage = function sendMessage(elements) {
  1032. // Do nothing
  1033. };
  1034. this.connect = function connect(data) {
  1035. // Ensure any existing connection is killed
  1036. tunnel.disconnect();
  1037. // Connection is now starting
  1038. tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  1039. // Create Guacamole protocol and UTF-8 parsers specifically for this
  1040. // connection
  1041. var parser = new Guacamole.Parser();
  1042. var utf8Parser = new Guacamole.UTF8Parser();
  1043. // Invoke tunnel's oninstruction handler for each parsed instruction
  1044. parser.oninstruction = function instructionReceived(opcode, args) {
  1045. if (tunnel.oninstruction)
  1046. tunnel.oninstruction(opcode, args);
  1047. };
  1048. // Allow new request to be aborted
  1049. abortController = new AbortController();
  1050. // Stream using the Fetch API
  1051. fetch(url, {
  1052. headers : extraHeaders,
  1053. credentials : crossDomain ? 'include' : 'same-origin',
  1054. signal : abortController.signal
  1055. })
  1056. .then(function gotResponse(response) {
  1057. // Reset state and close upon error
  1058. if (!response.ok) {
  1059. if (tunnel.onerror)
  1060. tunnel.onerror(new Guacamole.Status(
  1061. Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText));
  1062. tunnel.disconnect();
  1063. return;
  1064. }
  1065. // Report overall size of stream in bytes, if known
  1066. tunnel.size = response.headers.get('Content-Length');
  1067. // Connection is open
  1068. tunnel.setState(Guacamole.Tunnel.State.OPEN);
  1069. var reader = response.body.getReader();
  1070. var processReceivedText = function processReceivedText(result) {
  1071. // Clean up and close when done
  1072. if (result.done) {
  1073. tunnel.disconnect();
  1074. return;
  1075. }
  1076. // Parse only the portion of data which is newly received
  1077. parser.receive(utf8Parser.decode(result.value));
  1078. // Continue parsing when next chunk is received
  1079. reader.read().then(processReceivedText);
  1080. };
  1081. // Schedule parse of first chunk
  1082. reader.read().then(processReceivedText);
  1083. });
  1084. };
  1085. this.disconnect = function disconnect() {
  1086. // Abort any in-progress request
  1087. if (abortController) {
  1088. abortController.abort();
  1089. abortController = null;
  1090. }
  1091. // Connection is now closed
  1092. tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  1093. };
  1094. };
  1095. Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel();