diff --git a/webclients/novnc/LICENSE.txt b/webclients/novnc/LICENSE.txt index 67cdca5..2d09408 100644 --- a/webclients/novnc/LICENSE.txt +++ b/webclients/novnc/LICENSE.txt @@ -1,7 +1,7 @@ noVNC is Copyright (C) 2011 Joel Martin -The noVNC core library is licensed under the LGPLv3 (GNU Lesser -General Public License). The noVNC core library is composed of the +The noVNC core library files are licensed under the MPL 2.0 (Mozilla +Public License 2.0). The noVNC core library is composed of the Javascript code necessary for full noVNC operation. This includes (but is not limited to): @@ -10,6 +10,7 @@ is not limited to): include/display.js include/input.js include/jsunzip.js + include/keysym.js include/logo.js include/rfb.js include/ui.js @@ -36,21 +37,15 @@ The HTML, CSS, font and image files are licensed as follows: images/ : Creative Commons Attribution-ShareAlike http://creativecommons.org/licenses/by-sa/3.0/ -In addition the following file, which is part of the noVNC core -library, may be licensed under either the LGPL-2, LGPL-3 or MPL 2.0 -when it used separately from the noVNC core library. - - include/input.js : LGPL-2 or any later version - Some portions of noVNC are copyright to their individual authors. Please refer to the individual source files and/or to the noVNC commit history: https://github.com/kanaka/noVNC/commits/master The are several files and projects that have been incorporated into the noVNC core library. Here is a list of those files and the original -licenses (all LGPL-3 compatible): +licenses (all MPL 2.0 compatible): - include/base64.js : MPL 1.1, GPL-2 or LGPL-2.1 + include/base64.js : MPL 2.0 include/des.js : Various BSD style licenses @@ -59,20 +54,29 @@ licenses (all LGPL-3 compatible): include/web-socket-js/ : New BSD license (3-clause). Source code at http://github.com/gimite/web-socket-js + include/chrome-app/tcp-stream.js + : Apache 2.0 license + + utils/websockify + utils/websocket.py : LGPL 3 + The following license texts are included: + docs/LICENSE.MPL-2.0 docs/LICENSE.LGPL-3 and docs/LICENSE.GPL-3 docs/LICENSE.OFL-1.1 docs/LICENSE.BSD-3-Clause (New BSD) docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD) docs/LICENSE.zlib - docs/LICENSE.MPL-2.0 + docs/LICENSE.Apache-2.0 Or alternatively the license texts may be found here: + http://www.mozilla.org/MPL/2.0/ http://www.gnu.org/licenses/lgpl.html and http://www.gnu.org/licenses/gpl.html http://scripts.sil.org/OFL - http://www.mozilla.org/MPL/1.1/ - http://www.mozilla.org/MPL/2.0/ + http://en.wikipedia.org/wiki/BSD_licenses + http://www.gzip.org/zlib/zlib_license.html + http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/webclients/novnc/README.md b/webclients/novnc/README.md index 9c14e7d..b5679cd 100644 --- a/webclients/novnc/README.md +++ b/webclients/novnc/README.md @@ -1,16 +1,45 @@ ## noVNC: HTML5 VNC Client +[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=master)](https://travis-ci.org/kanaka/noVNC) ### Description noVNC is a HTML5 VNC client that runs well in any modern browser including mobile browsers (iPhone/iPad and Android). +Many companies/projects have integrated noVNC including [Ganeti Web +Manager](http://code.osuosl.org/projects/ganeti-webmgr), +[OpenStack](http://www.openstack.org), +[OpenNebula](http://opennebula.org/), and +[LibVNCServer](http://libvncserver.sourceforge.net). See [the Projects +and Companies wiki +page](https://github.com/kanaka/noVNC/wiki/ProjectsCompanies-using-noVNC) +for a more complete list with additional info and links. + +### News/help/contact + Notable commits, announcements and news are posted to -@noVNC +@noVNC + +If you are a noVNC developer/integrator/user (or want to be) please +join the noVNC +discussion group + +Bugs and feature requests can be submitted via [github +issues](https://github.com/kanaka/noVNC/issues). If you are looking +for a place to start contributing to noVNC, a good place to start +would be the issues that are marked as +["patchwelcome"](https://github.com/kanaka/noVNC/issues?labels=patchwelcome). -There are many companies/projects that have integrated noVNC into -their products including: [Ganeti Web Manager](http://code.osuosl.org/projects/ganeti-webmgr), [Archipel](http://archipelproject.org), [openQRM](http://www.openqrm.com/), [OpenNode](http://www.opennodecloud.com/), [OpenStack](http://www.openstack.org), [Broadway (HTML5 GDK/GTK+ backend)](http://blogs.gnome.org/alexl/2011/03/15/gtk-html-backend-update/), [OpenNebula](http://opennebula.org/), [CloudSigma](http://www.cloudsigma.com/), [Zentyal (formerly eBox)](http://www.zentyal.org/), [SlapOS](http://www.slapos.org), [Intel MeshCentral](https://meshcentral.com), [Amahi](http://amahi.org), [Brightbox](http://brightbox.com/), [Foreman](http://theforeman.org), [LibVNCServer](http://libvnc.github.io/) and [PocketVNC](http://www.pocketvnc.com/blog/?page_id=866). See [this wiki page](https://github.com/kanaka/noVNC/wiki/ProjectsCompanies-using-noVNC) for more info and links. +If you want to show appreciation for noVNC you could donate to a great +non-profits such as: [Compassion +International](http://www.compassion.com/), [SIL](http://www.sil.org), +[Habitat for Humanity](http://www.habitat.org), [Electronic Frontier +Foundation](https://www.eff.org/), [Against Malaria +Foundation](http://www.againstmalaria.com/), [Nothing But +Nets](http://www.nothingbutnets.net/), etc. Please tweet @noVNC if you do. ### Features @@ -24,7 +53,7 @@ their products including: [Ganeti Web Manager](http://code.osuosl.org/projects/g * Clipboard copy/paste * Clipping or scolling modes for large remote screens * Easy site integration and theming (3 example themes included) -* Licensed under the [LGPLv3](http://www.gnu.org/licenses/lgpl.html) +* Licensed under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/) ### Screenshots @@ -49,17 +78,18 @@ See more screenshots h * Fast Javascript Engine: this is not strictly a requirement, but without a fast Javascript engine, noVNC might be painfully slow. -* I maintain a more detailed browser compatibility list here. +* See the more detailed [browser compatibility wiki page](https://github.com/kanaka/noVNC/wiki/Browser-support). ### Server Requirements Unless you are using a VNC server with support for WebSockets -connections (such as [x11vnc/libvncserver](http://libvnc.github.io/) or -[PocketVNC](http://www.pocketvnc.com/blog/?page_id=866)), -you need to use a WebSockets to TCP socket proxy. There is -a python proxy included ('websockify'). +connections (such as +[x11vnc/libvncserver](http://libvncserver.sourceforge.net/), +[QEMU](http://www.qemu.org/), or +[PocketVNC](http://www.pocketvnc.com/blog/?page_id=866)), you need to +use a WebSockets to TCP socket proxy. There is a python proxy included +('websockify'). ### Quick Start @@ -88,8 +118,14 @@ a python proxy included ('websockify'). ### Authors/Contributors -* noVNC : Joel Martin (github.com/kanaka) - * New UI and Icons : Chris Gordon +* Core team: + * [Joel Martin](https://github.com/kanaka) + * [Samuel Mannehed](https://github.com/samhed) (Cendio) + * [Peter Åstrand](https://github.com/astrand) (Cendio) + * [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack) + +* Notable contributions: + * UI and Icons : Chris Gordon * Original Logo : Michael Sersen * tight encoding : Michael Tinglof (Mercuri.ca) @@ -100,5 +136,3 @@ a python proxy included ('websockify'). * jsunzip : Erik Moller (github.com/operasoftware/jsunzip), * tinflate : Joergen Ibsen (ibsensoftware.com) * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs) - - diff --git a/webclients/novnc/images/alt.png b/webclients/novnc/images/alt.png new file mode 100644 index 0000000..d42af7b Binary files /dev/null and b/webclients/novnc/images/alt.png differ diff --git a/webclients/novnc/images/ctrl.png b/webclients/novnc/images/ctrl.png new file mode 100644 index 0000000..a63b601 Binary files /dev/null and b/webclients/novnc/images/ctrl.png differ diff --git a/webclients/novnc/images/esc.png b/webclients/novnc/images/esc.png new file mode 100644 index 0000000..ece5f7c Binary files /dev/null and b/webclients/novnc/images/esc.png differ diff --git a/webclients/novnc/images/power.png b/webclients/novnc/images/power.png new file mode 100644 index 0000000..f68fd08 Binary files /dev/null and b/webclients/novnc/images/power.png differ diff --git a/webclients/novnc/images/showextrakeys.png b/webclients/novnc/images/showextrakeys.png new file mode 100644 index 0000000..ad8e0a7 Binary files /dev/null and b/webclients/novnc/images/showextrakeys.png differ diff --git a/webclients/novnc/images/tab.png b/webclients/novnc/images/tab.png new file mode 100644 index 0000000..8413487 Binary files /dev/null and b/webclients/novnc/images/tab.png differ diff --git a/webclients/novnc/include/base.css b/webclients/novnc/include/base.css index 3a2feb3..e2c9a96 100644 --- a/webclients/novnc/include/base.css +++ b/webclients/novnc/include/base.css @@ -1,7 +1,8 @@ /* * noVNC base CSS * Copyright (C) 2012 Joel Martin - * noVNC is licensed under the LGPL-3 (see LICENSE.txt) + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). */ @@ -40,9 +41,6 @@ html { } #noVNC_encrypt { } -#noVNC_connectTimeout { - width: 30px; -} #noVNC_path { width: 100px; } @@ -51,6 +49,9 @@ html { float:right; } +#noVNC_buttons { + white-space: nowrap; +} #noVNC_view_drag_button { display: none; @@ -58,38 +59,43 @@ html { #sendCtrlAltDelButton { display: none; } +#noVNC_xvp_buttons { + display: none; +} #noVNC_mobile_buttons { display: none; } +#noVNC_extra_keys { + display: inline; + list-style-type: none; + padding: 0px; + margin: 0px; + position: relative; +} + .noVNC-buttons-left { float: left; - padding-left:10px; - padding-top:4px; + z-index: 1; + position: relative; } .noVNC-buttons-right { float:right; right: 0px; - padding-right:10px; - padding-top:4px; + z-index: 2; + position: absolute; } -#noVNC_status_bar { - margin-top: 0px; - padding: 0px; -} - -#noVNC_status_bar div { +#noVNC_status { font-size: 12px; padding-top: 4px; - width:100%; -} - -#noVNC_status { - height:20px; + height:32px; text-align: center; + font-weight: bold; + color: #fff; } + #noVNC_settings_menu { margin: 3px; text-align: left; @@ -104,22 +110,12 @@ html { float:right; } -.noVNC_status_normal { - background: #eee; -} -.noVNC_status_error { - background: #f44; -} -.noVNC_status_warn { - background: #ff4; -} - /* Do not set width/height for VNC_screen or VNC_canvas or incorrect * scaling will occur. Canvas resizes to remote VNC settings */ #noVNC_screen_pad { margin: 0px; padding: 0px; - height: 44px; + height: 36px; } #noVNC_screen { text-align: center; @@ -154,14 +150,14 @@ html { /*Bubble contents divs*/ #noVNC_settings { display:none; - margin-top:77px; + margin-top:73px; right:20px; position:fixed; } #noVNC_controls { display:none; - margin-top:77px; + margin-top:73px; right:12px; position:fixed; } @@ -173,7 +169,7 @@ html { display:none; position:fixed; - margin-top:77px; + margin-top:73px; right:20px; left:20px; padding:15px; @@ -186,9 +182,40 @@ html { border-radius:10px; } +#noVNC_popup_status_panel { + display:none; + position: fixed; + z-index: 1; + + margin:15px; + margin-top:60px; + padding:15px; + width:auto; + + text-align:center; + font-weight:bold; + word-wrap:break-word; + color:#fff; + background:rgba(0,0,0,0.65); + + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; +} + +#noVNC_xvp { + display:none; + margin-top:73px; + right:30px; + position:fixed; +} +#noVNC_xvp.top:after { + right:125px; +} + #noVNC_clipboard { display:none; - margin-top:77px; + margin-top:73px; right:30px; position:fixed; } @@ -207,17 +234,11 @@ html { z-index: -1; } -.noVNC_status_warn { - background-color:yellow; -} - /* * Advanced Styling */ -/* Control bar */ -#noVNC-control-bar { - position:fixed; +.noVNC_status_normal { background: #b2bdcd; /* Old browsers */ background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ @@ -225,9 +246,32 @@ html { background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +/* Control bar */ +#noVNC-control-bar { + position:fixed; + display:block; - height:44px; + height:36px; left:0; top:0; width:100%; @@ -368,22 +412,85 @@ html { font-size: 180px; } -@media screen and (min-width: 481px) and (max-width: 640px) { - .noVNC_status_button { - font-size: 10px; +.noVNC-buttons-left { + padding-left: 10px; +} + +.noVNC-buttons-right { + padding-right: 10px; +} + +#noVNC_status { + z-index: 0; + position: absolute; + width: 100%; + margin-left: 0px; +} + +#showExtraKeysButton { display: none; } +#toggleCtrlButton { display: inline; } +#toggleAltButton { display: inline; } +#sendTabButton { display: inline; } +#sendEscButton { display: inline; } + +/* left-align the status text on lower resolutions */ +@media screen and (max-width: 800px){ + #noVNC_status { + z-index: 1; + position: relative; + width: auto; + float: left; + margin-left: 4px; } +} + +@media screen and (max-width: 640px){ #noVNC_clipboard_text { width: 410px; } #noVNC_logo { font-size: 150px; } -} - -@media screen and (min-width: 321px) and (max-width: 480px) { .noVNC_status_button { font-size: 10px; } + .noVNC-buttons-left { + padding-left: 0px; + } + .noVNC-buttons-right { + padding-right: 0px; + } + /* collapse the extra keys on lower resolutions */ + #showExtraKeysButton { + display: inline; + } + #toggleCtrlButton { + display: none; + position: absolute; + top: 30px; + left: 0px; + } + #toggleAltButton { + display: none; + position: absolute; + top: 65px; + left: 0px; + } + #sendTabButton { + display: none; + position: absolute; + top: 100px; + left: 0px; + } + #sendEscButton { + display: none; + position: absolute; + top: 135px; + left: 0px; + } +} + +@media screen and (min-width: 321px) and (max-width: 480px) { #noVNC_clipboard_text { width: 250px; } diff --git a/webclients/novnc/include/base64.js b/webclients/novnc/include/base64.js index e9b3c52..651fbad 100644 --- a/webclients/novnc/include/base64.js +++ b/webclients/novnc/include/base64.js @@ -1,147 +1,113 @@ -/* - * Modified from: - * http://lxr.mozilla.org/mozilla/source/extensions/xml-rpc/src/nsXmlRpcClient.js#956 - */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Mozilla XML-RPC Client component. - * - * The Initial Developer of the Original Code is - * Digital Creations 2, Inc. - * Portions created by the Initial Developer are Copyright (C) 2000 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Martijn Pieters (original author) - * Samuel Sieb - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ +// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js -/*jslint white: false, bitwise: false, plusplus: false */ +/*jslint white: false */ /*global console */ var Base64 = { - -/* Convert data (an array of integers) to a Base64 string. */ -toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', -base64Pad : '=', - -encode: function (data) { - "use strict"; - var result = '', - chrTable = Base64.toBase64Table.split(''), - pad = Base64.base64Pad, - length = data.length, - i; - // Convert every three bytes to 4 ascii characters. - for (i = 0; i < (length - 2); i += 3) { - result += chrTable[data[i] >> 2]; - result += chrTable[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; - result += chrTable[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; - result += chrTable[data[i+2] & 0x3f]; - } - - // Convert the remaining 1 or 2 bytes, pad out to 4 characters. - if (length%3) { - i = length - (length%3); - result += chrTable[data[i] >> 2]; - if ((length%3) === 2) { - result += chrTable[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; - result += chrTable[(data[i+1] & 0x0f) << 2]; - result += pad; - } else { - result += chrTable[(data[i] & 0x03) << 4]; - result += pad + pad; + /* Convert data (an array of integers) to a Base64 string. */ + toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), + base64Pad : '=', + + encode: function (data) { + "use strict"; + var result = ''; + var toBase64Table = Base64.toBase64Table; + var length = data.length; + var lengthpad = (length % 3); + // Convert every three bytes to 4 ascii characters. + + for (var i = 0; i < (length - 2); i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; } - } - - return result; -}, - -/* Convert Base64 data to a string */ -toBinaryTable : [ - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, - 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, - 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, - -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, - 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 -], - -decode: function (data, offset) { - "use strict"; - offset = typeof(offset) !== 'undefined' ? offset : 0; - var binTable = Base64.toBinaryTable, - pad = Base64.base64Pad, - result, result_length, idx, i, c, padding, - leftbits = 0, // number of bits decoded, but yet to be appended - leftdata = 0, // bits decoded, but yet to be appended - data_length = data.indexOf('=') - offset; - if (data_length < 0) { data_length = data.length - offset; } - - /* Every four characters is 3 resulting numbers */ - result_length = (data_length >> 2) * 3 + Math.floor((data_length%4)/1.5); - result = new Array(result_length); - - // Convert one by one. - for (idx = 0, i = offset; i < data.length; i++) { - c = binTable[data.charCodeAt(i) & 0x7f]; - padding = (data.charAt(i) === pad); - // Skip illegal characters and whitespace - if (c === -1) { - console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); - continue; + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + var j = 0; + if (lengthpad === 2) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; + result += toBase64Table[(data[j + 1] & 0x0f) << 2]; + result += toBase64Table[64]; + } else if (lengthpad === 1) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[(data[j] & 0x03) << 4]; + result += toBase64Table[64]; + result += toBase64Table[64]; } - - // Collect data into leftdata, update bitcount - leftdata = (leftdata << 6) | c; - leftbits += 6; - // If we have 8 or more bits, append 8 bits to the result - if (leftbits >= 8) { - leftbits -= 8; - // Append if not padding. - if (!padding) { - result[idx++] = (leftdata >> leftbits) & 0xff; + return result; + }, + + /* Convert Base64 data to a string */ + /* jshint -W013 */ + toBinaryTable : [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 + ], + /* jshint +W013 */ + + decode: function (data, offset) { + "use strict"; + offset = typeof(offset) !== 'undefined' ? offset : 0; + var toBinaryTable = Base64.toBinaryTable; + var base64Pad = Base64.base64Pad; + var result, result_length; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + var data_length = data.indexOf('=') - offset; + + if (data_length < 0) { data_length = data.length - offset; } + + /* Every four characters is 3 resulting numbers */ + result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); + result = new Array(result_length); + + // Convert one by one. + for (var idx = 0, i = offset; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data.charAt(i) === base64Pad); + // Skip illegal characters and whitespace + if (c === -1) { + console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; } - leftdata &= (1 << leftbits) - 1; } - } - // If there are any bits left, the base64 string was corrupted - if (leftbits) { - throw {name: 'Base64-Error', - message: 'Corrupted base64 string'}; - } - - return result; -} + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + err = new Error('Corrupted base64 string'); + err.name = 'Base64-Error'; + throw err; + } + return result; + } }; /* End of Base64 namespace */ diff --git a/webclients/novnc/include/black.css b/webclients/novnc/include/black.css index e958ee3..7d940c5 100644 --- a/webclients/novnc/include/black.css +++ b/webclients/novnc/include/black.css @@ -1,7 +1,8 @@ /* - * noVNC base CSS + * noVNC black CSS * Copyright (C) 2012 Joel Martin - * noVNC is licensed under the LGPL-3 (see LICENSE.txt) + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). */ @@ -9,7 +10,7 @@ background-color:#000; } -#noVNC-control-bar { +.noVNC_status_normal { background: #4c4c4c; /* Old browsers */ background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ @@ -18,6 +19,24 @@ background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ } +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} .triangle-right { border:2px solid #fff; diff --git a/webclients/novnc/include/blue.css b/webclients/novnc/include/blue.css index 3dad0b4..b2a0adc 100644 --- a/webclients/novnc/include/blue.css +++ b/webclients/novnc/include/blue.css @@ -1,11 +1,12 @@ /* - * noVNC base CSS + * noVNC blue CSS * Copyright (C) 2012 Joel Martin - * noVNC is licensed under the LGPL-3 (see LICENSE.txt) + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). */ -#noVNC-control-bar { +.noVNC_status_normal { background-color:#04073d; background-image: -webkit-gradient( linear, @@ -20,6 +21,36 @@ rgb(4,7,61) 50% ); } +.noVNC_status_error { + background-color:#f04040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,64,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(249,64,64) 50% + ); +} +.noVNC_status_warn { + background-color:#f0f040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,240,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(240,240,64) 50% + ); +} .triangle-right { border:2px solid #fff; diff --git a/webclients/novnc/include/chrome-app/tcp-client.js b/webclients/novnc/include/chrome-app/tcp-client.js new file mode 100644 index 0000000..b8c125f --- /dev/null +++ b/webclients/novnc/include/chrome-app/tcp-client.js @@ -0,0 +1,321 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Boris Smus (smus@chromium.org) +*/ + +(function(exports) { + + // Define some local variables here. + var socket = chrome.socket || chrome.experimental.socket; + var dns = chrome.experimental.dns; + + /** + * Creates an instance of the client + * + * @param {String} host The remote host to connect to + * @param {Number} port The port to connect to at the remote host + */ + function TcpClient(host, port, pollInterval) { + this.host = host; + this.port = port; + this.pollInterval = pollInterval || 15; + + // Callback functions. + this.callbacks = { + connect: null, // Called when socket is connected. + disconnect: null, // Called when socket is disconnected. + recvBuffer: null, // Called (as ArrayBuffer) when client receives data from server. + recvString: null, // Called (as string) when client receives data from server. + sent: null // Called when client sends data to server. + }; + + // Socket. + this.socketId = null; + this.isConnected = false; + + log('initialized tcp client'); + } + + /** + * Connects to the TCP socket, and creates an open socket. + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-create + * @param {Function} callback The function to call on connection + */ + TcpClient.prototype.connect = function(callback) { + // First resolve the hostname to an IP. + dns.resolve(this.host, function(result) { + this.addr = result.address; + socket.create('tcp', {}, this._onCreate.bind(this)); + + // Register connect callback. + this.callbacks.connect = callback; + }.bind(this)); + }; + + /** + * Sends an arraybuffer/view down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The arraybuffer/view to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendBuffer = function(buf, callback) { + if (buf.buffer) { + buf = buf.buffer; + } + + /* + // Debug + var bytes = [], u8 = new Uint8Array(buf); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("sending bytes: " + (bytes.join(','))); + */ + + socket.write(this.socketId, buf, this._onWriteComplete.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sends a string down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The string to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendString = function(msg, callback) { + /* + // Debug + log("sending string: " + msg); + */ + + this._stringToArrayBuffer(msg, function(arrayBuffer) { + socket.write(this.socketId, arrayBuffer, this._onWriteComplete.bind(this)); + }.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sets the callback for when a message is received + * + * @param {Function} callback The function to call when a message has arrived + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addResponseListener = function(callback, type) { + if (typeof type === "undefined") { + type = "arraybuffer"; + } + // Register received callback. + if (type === "string") { + this.callbacks.recvString = callback; + } else { + this.callbacks.recvBuffer = callback; + } + }; + + /** + * Sets the callback for when the socket disconnects + * + * @param {Function} callback The function to call when the socket disconnects + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addDisconnectListener = function(callback) { + // Register disconnect callback. + this.callbacks.disconnect = callback; + }; + + /** + * Disconnects from the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-disconnect + */ + TcpClient.prototype.disconnect = function() { + if (this.isConnected) { + this.isConnected = false; + socket.disconnect(this.socketId); + if (this.callbacks.disconnect) { + this.callbacks.disconnect(); + } + log('socket disconnected'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * create a socket. If the socket is successfully created + * we go ahead and connect to the remote side. + * + * @private + * @see http://developer.chrome.com/trunk/apps/socket.html#method-connect + * @param {Object} createInfo The socket details + */ + TcpClient.prototype._onCreate = function(createInfo) { + this.socketId = createInfo.socketId; + if (this.socketId > 0) { + socket.connect(this.socketId, this.addr, this.port, this._onConnectComplete.bind(this)); + } else { + error('Unable to create socket'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * connect to the remote side. If a successful connection is + * made then polling starts to check for data to read + * + * @private + * @param {Number} resultCode Indicates whether the connection was successful + */ + TcpClient.prototype._onConnectComplete = function(resultCode) { + // Start polling for reads. + this.isConnected = true; + setTimeout(this._periodicallyRead.bind(this), this.pollInterval); + + if (this.callbacks.connect) { + log('connect complete'); + this.callbacks.connect(); + } + log('onConnectComplete'); + }; + + /** + * Checks for new data to read from the socket + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-read + */ + TcpClient.prototype._periodicallyRead = function() { + var that = this; + socket.getInfo(this.socketId, function (info) { + if (info.connected) { + setTimeout(that._periodicallyRead.bind(that), that.pollInterval); + socket.read(that.socketId, null, that._onDataRead.bind(that)); + } else if (that.isConnected) { + log('socket disconnect detected'); + that.disconnect(); + } + }); + }; + + /** + * Callback function for when data has been read from the socket. + * Converts the array buffer that is read in to a string + * and sends it on for further processing by passing it to + * the previously assigned callback function. + * + * @private + * @see TcpClient.prototype.addResponseListener + * @param {Object} readInfo The incoming message + */ + TcpClient.prototype._onDataRead = function(readInfo) { + // Call received callback if there's data in the response. + if (readInfo.resultCode > 0) { + log('onDataRead'); + + /* + // Debug + var bytes = [], u8 = new Uint8Array(readInfo.data); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("received bytes: " + (bytes.join(','))); + */ + + if (this.callbacks.recvBuffer) { + // Return raw ArrayBuffer directly. + this.callbacks.recvBuffer(readInfo.data); + } + if (this.callbacks.recvString) { + // Convert ArrayBuffer to string. + this._arrayBufferToString(readInfo.data, function(str) { + this.callbacks.recvString(str); + }.bind(this)); + } + + // Trigger another read right away + setTimeout(this._periodicallyRead.bind(this), 0); + } + }; + + /** + * Callback for when data has been successfully + * written to the socket. + * + * @private + * @param {Object} writeInfo The outgoing message + */ + TcpClient.prototype._onWriteComplete = function(writeInfo) { + log('onWriteComplete'); + // Call sent callback. + if (this.callbacks.sent) { + this.callbacks.sent(writeInfo); + } + }; + + /** + * Converts an array buffer to a string + * + * @private + * @param {ArrayBuffer} buf The buffer to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._arrayBufferToString = function(buf, callback) { + var bb = new Blob([new Uint8Array(buf)]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsText(bb); + }; + + /** + * Converts a string to an array buffer + * + * @private + * @param {String} str The string to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._stringToArrayBuffer = function(str, callback) { + var bb = new Blob([str]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsArrayBuffer(bb); + }; + + /** + * Wrapper function for logging + */ + function log(msg) { + console.log(msg); + } + + /** + * Wrapper function for error logging + */ + function error(msg) { + console.error(msg); + } + + exports.TcpClient = TcpClient; + +})(window); diff --git a/webclients/novnc/include/des.js b/webclients/novnc/include/des.js index 1f95285..ecbc819 100644 --- a/webclients/novnc/include/des.js +++ b/webclients/novnc/include/des.js @@ -75,199 +75,202 @@ * fine Java utilities: http://www.acme.com/java/ */ -"use strict"; -/*jslint white: false, bitwise: false, plusplus: false */ +/* jslint white: false */ function DES(passwd) { + "use strict"; -// Tables, permutations, S-boxes, etc. -var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, - 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, - 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], - totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], - z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, - keys = []; + // Tables, permutations, S-boxes, etc. + // jshint -W013 + var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], + z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, + keys = []; -a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; -SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, - z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, - a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, - c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; -a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; -SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, - a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, - z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, - z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; -a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; -SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, - b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, - c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, - b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; -a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; -SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, - z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, - b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, - c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; -a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; -SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, - a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, - z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, - c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; -a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; -SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, - z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, - b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, - a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; -a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; -SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, - b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, - b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, - z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; -a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; -SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, - c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, - a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, - z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + // jshint -W015 + a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; + SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, + z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, + a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, + c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; + a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; + SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, + a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, + z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, + z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; + a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; + SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, + b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, + c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, + b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; + a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; + SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, + z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, + b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, + c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; + a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; + SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, + a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, + z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, + c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; + a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; + SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, + z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, + b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, + a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; + a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; + SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, + b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, + b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, + z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; + a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; + SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, + c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, + a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, + z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + // jshint +W013,+W015 -// Set the key. -function setKeys(keyBlock) { - var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], - raw0, raw1, rawi, KnLi; + // Set the key. + function setKeys(keyBlock) { + var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], + raw0, raw1, rawi, KnLi; - for (j = 0, l = 56; j < 56; ++j, l-=8) { - l += l<-5 ? 65 : l<-3 ? 31 : l<-1 ? 63 : l===27 ? 35 : 0; // PC1 - m = l & 0x7; - pc1m[j] = ((keyBlock[l >>> 3] & (1<>> 3] & (1<>> 10; - keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; - ++KnLi; - keys[KnLi] = (raw0 & 0x0003f000) << 12; - keys[KnLi] |= (raw0 & 0x0000003f) << 16; - keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; - keys[KnLi] |= (raw1 & 0x0000003f); - ++KnLi; + // cookey + for (i = 0, rawi = 0, KnLi = 0; i < 16; ++i) { + raw0 = kn[rawi++]; + raw1 = kn[rawi++]; + keys[KnLi] = (raw0 & 0x00fc0000) << 6; + keys[KnLi] |= (raw0 & 0x00000fc0) << 10; + keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10; + keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + keys[KnLi] = (raw0 & 0x0003f000) << 12; + keys[KnLi] |= (raw0 & 0x0000003f) << 16; + keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } } -} -// Encrypt 8 bytes of text -function enc8(text) { - var i = 0, b = text.slice(), fval, keysi = 0, - l, r, x; // left, right, accumulator + // Encrypt 8 bytes of text + function enc8(text) { + var i = 0, b = text.slice(), fval, keysi = 0, + l, r, x; // left, right, accumulator - // Squash 8 bytes to 2 ints - l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; - r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + // Squash 8 bytes to 2 ints + l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; - x = ((l >>> 4) ^ r) & 0x0f0f0f0f; - r ^= x; - l ^= (x << 4); - x = ((l >>> 16) ^ r) & 0x0000ffff; - r ^= x; - l ^= (x << 16); - x = ((r >>> 2) ^ l) & 0x33333333; - l ^= x; - r ^= (x << 2); - x = ((r >>> 8) ^ l) & 0x00ff00ff; - l ^= x; - r ^= (x << 8); - r = (r << 1) | ((r >>> 31) & 1); - x = (l ^ r) & 0xaaaaaaaa; - l ^= x; - r ^= x; - l = (l << 1) | ((l >>> 31) & 1); + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); - for (i = 0; i < 8; ++i) { - x = (r << 28) | (r >>> 4); - x ^= keys[keysi++]; - fval = SP7[x & 0x3f]; - fval |= SP5[(x >>> 8) & 0x3f]; - fval |= SP3[(x >>> 16) & 0x3f]; - fval |= SP1[(x >>> 24) & 0x3f]; - x = r ^ keys[keysi++]; - fval |= SP8[x & 0x3f]; - fval |= SP6[(x >>> 8) & 0x3f]; - fval |= SP4[(x >>> 16) & 0x3f]; - fval |= SP2[(x >>> 24) & 0x3f]; - l ^= fval; - x = (l << 28) | (l >>> 4); - x ^= keys[keysi++]; - fval = SP7[x & 0x3f]; - fval |= SP5[(x >>> 8) & 0x3f]; - fval |= SP3[(x >>> 16) & 0x3f]; - fval |= SP1[(x >>> 24) & 0x3f]; - x = l ^ keys[keysi++]; - fval |= SP8[x & 0x0000003f]; - fval |= SP6[(x >>> 8) & 0x3f]; - fval |= SP4[(x >>> 16) & 0x3f]; - fval |= SP2[(x >>> 24) & 0x3f]; - r ^= fval; - } + for (i = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } - r = (r << 31) | (r >>> 1); - x = (l ^ r) & 0xaaaaaaaa; - l ^= x; - r ^= x; - l = (l << 31) | (l >>> 1); - x = ((l >>> 8) ^ r) & 0x00ff00ff; - r ^= x; - l ^= (x << 8); - x = ((l >>> 2) ^ r) & 0x33333333; - r ^= x; - l ^= (x << 2); - x = ((r >>> 16) ^ l) & 0x0000ffff; - l ^= x; - r ^= (x << 16); - x = ((r >>> 4) ^ l) & 0x0f0f0f0f; - l ^= x; - r ^= (x << 4); + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); - // Spread ints to bytes - x = [r, l]; - for (i = 0; i < 8; i++) { - b[i] = (x[i>>>2] >>> (8*(3 - (i%4)))) % 256; - if (b[i] < 0) { b[i] += 256; } // unsigned + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; } - return b; -} -// Encrypt 16 bytes of text using passwd as key -function encrypt(t) { - return enc8(t.slice(0,8)).concat(enc8(t.slice(8,16))); -} + // Encrypt 16 bytes of text using passwd as key + function encrypt(t) { + return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16))); + } -setKeys(passwd); // Setup keys -return {'encrypt': encrypt}; // Public interface + setKeys(passwd); // Setup keys + return {'encrypt': encrypt}; // Public interface } // function DES diff --git a/webclients/novnc/include/display.js b/webclients/novnc/include/display.js index 5ad99ba..8763fa4 100644 --- a/webclients/novnc/include/display.js +++ b/webclients/novnc/include/display.js @@ -1,757 +1,740 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin - * Licensed under LGPL-3 (see LICENSE.txt) + * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. */ -/*jslint browser: true, white: false, bitwise: false */ +/*jslint browser: true, white: false */ /*global Util, Base64, changeCursor */ -function Display(defaults) { -"use strict"; - -var that = {}, // Public API methods - conf = {}, // Configuration attributes - - // Private Display namespace variables - c_ctx = null, - c_forceCanvas = false, - - // Queued drawing actions for in-order rendering - renderQ = [], - - // Predefine function variables (jslint) - imageDataGet, rgbImageData, bgrxImageData, cmapImageData, - setFillColor, rescale, scan_renderQ, - - // The full frame buffer (logical canvas) size - fb_width = 0, - fb_height = 0, - // The visible "physical canvas" viewport - viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 }, - cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1}, - - c_prevStyle = "", - tile = null, - tile16x16 = null, - tile_x = 0, - tile_y = 0; - - -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'wo', 'dom', null, 'Canvas element for rendering'], - ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'], - ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'], - ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'], - ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'], - ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'], - ['viewport', 'rw', 'bool', false, 'Use a viewport set with viewportChange()'], - ['width', 'rw', 'int', null, 'Display area width'], - ['height', 'rw', 'int', null, 'Display area height'], - - ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'], - - ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'], - ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI'] - ]); +var Display; -// Override some specific getters/setters -that.get_context = function () { return c_ctx; }; +(function () { + "use strict"; -that.set_scale = function(scale) { rescale(scale); }; + Display = function (defaults) { + this._drawCtx = null; + this._c_forceCanvas = false; -that.set_width = function (val) { that.resize(val, fb_height); }; -that.get_width = function() { return fb_width; }; + this._renderQ = []; // queue drawing actions for in-oder rendering -that.set_height = function (val) { that.resize(fb_width, val); }; -that.get_height = function() { return fb_height; }; + // the full frame buffer (logical canvas) size + this._fb_width = 0; + this._fb_height = 0; + // the visible "physical canvas" viewport + this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 }; + this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 }; + this._prevDrawStyle = ""; + this._tile = null; + this._tile16x16 = null; + this._tile_x = 0; + this._tile_y = 0; -// -// Private functions -// + Util.set_defaults(this, defaults, { + 'true_color': true, + 'colourMap': [], + 'scale': 1.0, + 'viewport': false, + 'render_mode': '' + }); -// Create the public API interface -function constructor() { - Util.Debug(">> Display.constructor"); + Util.Debug(">> Display.constructor"); - var c, func, i, curDat, curSave, - has_imageData = false, UE = Util.Engine; + if (!this._target) { + throw new Error("Target must be set"); + } - if (! conf.target) { throw("target must be set"); } + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } - if (typeof conf.target === 'string') { - throw("target must be a DOM element"); - } + if (!this._target.getContext) { + throw new Error("no getContext method"); + } - c = conf.target; + if (!this._drawCtx) { + this._drawCtx = this._target.getContext('2d'); + } - if (! c.getContext) { throw("no getContext method"); } + Util.Debug("User Agent: " + navigator.userAgent); + if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); } + if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); } + if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); } + if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); } - if (! c_ctx) { c_ctx = c.getContext('2d'); } + this.clear(); - Util.Debug("User Agent: " + navigator.userAgent); - if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); } - if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); } - if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); } - if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); } + // Check canvas features + if ('createImageData' in this._drawCtx) { + this._render_mode = 'canvas rendering'; + } else { + throw new Error("Canvas does not support createImageData"); + } - that.clear(); + if (this._prefer_js === null) { + Util.Info("Prefering javascript operations"); + this._prefer_js = true; + } - // Check canvas features - if ('createImageData' in c_ctx) { - conf.render_mode = "canvas rendering"; - } else { - throw("Canvas does not support createImageData"); - } - if (conf.prefer_js === null) { - Util.Info("Prefering javascript operations"); - conf.prefer_js = true; - } + // Determine browser support for setting the cursor via data URI scheme + var curDat = []; + for (var i = 0; i < 8 * 8 * 4; i++) { + curDat.push(255); + } + try { + var curSave = this._target.style.cursor; + Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8); + if (this._target.style.cursor) { + if (this._cursor_uri === null || this._cursor_uri === undefined) { + this._cursor_uri = true; + } + Util.Info("Data URI scheme cursor supported"); + } else { + if (this._cursor_uri === null || this._cursor_uri === undefined) { + this._cursor_uri = false; + } + Util.Warn("Data URI scheme cursor not supported"); + } + this._target.style.cursor = curSave; + } catch (exc) { + Util.Error("Data URI scheme cursor test exception: " + exc); + this._cursor_uri = false; + } - // Initialize cached tile imageData - tile16x16 = c_ctx.createImageData(16, 16); + Util.Debug("<< Display.constructor"); + }; - /* - * Determine browser support for setting the cursor via data URI - * scheme - */ - curDat = []; - for (i=0; i < 8 * 8 * 4; i += 1) { - curDat.push(255); - } - try { - curSave = c.style.cursor; - changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8); - if (c.style.cursor) { - if (conf.cursor_uri === null) { - conf.cursor_uri = true; + Display.prototype = { + // Public methods + viewportChange: function (deltaX, deltaY, width, height) { + var vp = this._viewportLoc; + var cr = this._cleanRect; + var canvas = this._target; + + if (!this._viewport) { + Util.Debug("Setting viewport to full display region"); + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; + width = this._fb_width; + height = this._fb_height; } - Util.Info("Data URI scheme cursor supported"); - } else { - if (conf.cursor_uri === null) { - conf.cursor_uri = false; + + if (typeof(deltaX) === "undefined") { deltaX = 0; } + if (typeof(deltaY) === "undefined") { deltaY = 0; } + if (typeof(width) === "undefined") { width = vp.w; } + if (typeof(height) === "undefined") { height = vp.h; } + + // Size change + if (width > this._fb_width) { width = this._fb_width; } + if (height > this._fb_height) { height = this._fb_height; } + + if (vp.w !== width || vp.h !== height) { + // Change width + if (width < vp.w && cr.x2 > vp.x + width - 1) { + cr.x2 = vp.x + width - 1; + } + vp.w = width; + + // Change height + if (height < vp.h && cr.y2 > vp.y + height - 1) { + cr.y2 = vp.y + height - 1; + } + vp.h = height; + + var saveImg = null; + if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { + var img_width = canvas.width < vp.w ? canvas.width : vp.w; + var img_height = canvas.height < vp.h ? canvas.height : vp.h; + saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); + } + + canvas.width = vp.w; + canvas.height = vp.h; + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } } - Util.Warn("Data URI scheme cursor not supported"); - } - c.style.cursor = curSave; - } catch (exc2) { - Util.Error("Data URI scheme cursor test exception: " + exc2); - conf.cursor_uri = false; - } - - Util.Debug("<< Display.constructor"); - return that ; -} - -rescale = function(factor) { - var c, tp, x, y, - properties = ['transform', 'WebkitTransform', 'MozTransform', null]; - c = conf.target; - tp = properties.shift(); - while (tp) { - if (typeof c.style[tp] !== 'undefined') { - break; - } - tp = properties.shift(); - } - - if (tp === null) { - Util.Debug("No scaling support"); - return; - } - - - if (typeof(factor) === "undefined") { - factor = conf.scale; - } else if (factor > 1.0) { - factor = 1.0; - } else if (factor < 0.1) { - factor = 0.1; - } - - if (conf.scale === factor) { - //Util.Debug("Display already scaled to '" + factor + "'"); - return; - } - - conf.scale = factor; - x = c.width - c.width * factor; - y = c.height - c.height * factor; - c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)"; -}; - -setFillColor = function(color) { - var bgr, newStyle; - if (conf.true_color) { - bgr = color; - } else { - bgr = conf.colourMap[color[0]]; - } - newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")"; - if (newStyle !== c_prevStyle) { - c_ctx.fillStyle = newStyle; - c_prevStyle = newStyle; - } -}; - - -// -// Public API interface functions -// - -// Shift and/or resize the visible viewport -that.viewportChange = function(deltaX, deltaY, width, height) { - var c = conf.target, v = viewport, cr = cleanRect, - saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h; - - if (!conf.viewport) { - Util.Debug("Setting viewport to full display region"); - deltaX = -v.w; // Clamped later if out of bounds - deltaY = -v.h; // Clamped later if out of bounds - width = fb_width; - height = fb_height; - } - - if (typeof(deltaX) === "undefined") { deltaX = 0; } - if (typeof(deltaY) === "undefined") { deltaY = 0; } - if (typeof(width) === "undefined") { width = v.w; } - if (typeof(height) === "undefined") { height = v.h; } - - // Size change - - if (width > fb_width) { width = fb_width; } - if (height > fb_height) { height = fb_height; } - - if ((v.w !== width) || (v.h !== height)) { - // Change width - if ((width < v.w) && (cr.x2 > v.x + width -1)) { - cr.x2 = v.x + width - 1; - } - v.w = width; - // Change height - if ((height < v.h) && (cr.y2 > v.y + height -1)) { - cr.y2 = v.y + height - 1; - } - v.h = height; + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + // Position change - if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) { - saveImg = c_ctx.getImageData(0, 0, - (c.width < v.w) ? c.width : v.w, - (c.height < v.h) ? c.height : v.h); - } + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; + } - c.width = v.w; - c.height = v.h; + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); + } - if (saveImg) { - c_ctx.putImageData(saveImg, 0, 0); - } - } - - vx2 = v.x + v.w - 1; - vy2 = v.y + v.h - 1; - - - // Position change - - if ((deltaX < 0) && ((v.x + deltaX) < 0)) { - deltaX = - v.x; - } - if ((vx2 + deltaX) >= fb_width) { - deltaX -= ((vx2 + deltaX) - fb_width + 1); - } - - if ((v.y + deltaY) < 0) { - deltaY = - v.y; - } - if ((vy2 + deltaY) >= fb_height) { - deltaY -= ((vy2 + deltaY) - fb_height + 1); - } - - if ((deltaX === 0) && (deltaY === 0)) { - //Util.Debug("skipping viewport change"); - return; - } - Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); - - v.x += deltaX; - vx2 += deltaX; - v.y += deltaY; - vy2 += deltaY; - - // Update the clean rectangle - if (v.x > cr.x1) { - cr.x1 = v.x; - } - if (vx2 < cr.x2) { - cr.x2 = vx2; - } - if (v.y > cr.y1) { - cr.y1 = v.y; - } - if (vy2 < cr.y2) { - cr.y2 = vy2; - } - - if (deltaX < 0) { - // Shift viewport left, redraw left section - x1 = 0; - w = - deltaX; - } else { - // Shift viewport right, redraw right section - x1 = v.w - deltaX; - w = deltaX; - } - if (deltaY < 0) { - // Shift viewport up, redraw top section - y1 = 0; - h = - deltaY; - } else { - // Shift viewport down, redraw bottom section - y1 = v.h - deltaY; - h = deltaY; - } - - // Copy the valid part of the viewport to the shifted location - saveStyle = c_ctx.fillStyle; - c_ctx.fillStyle = "rgb(255,255,255)"; - if (deltaX !== 0) { - //that.copyImage(0, 0, -deltaX, 0, v.w, v.h); - //that.fillRect(x1, 0, w, v.h, [255,255,255]); - c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h); - c_ctx.fillRect(x1, 0, w, v.h); - } - if (deltaY !== 0) { - //that.copyImage(0, 0, 0, -deltaY, v.w, v.h); - //that.fillRect(0, y1, v.w, h, [255,255,255]); - c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h); - c_ctx.fillRect(0, y1, v.w, h); - } - c_ctx.fillStyle = saveStyle; -}; - - -// Return a map of clean and dirty areas of the viewport and reset the -// tracking of clean and dirty areas. -// -// Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h}, -// 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]} -that.getCleanDirtyReset = function() { - var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [], - vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1; - - - // Copy the cleanRect - cleanBox = {'x': c.x1, 'y': c.y1, - 'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1}; - - if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) { - // Whole viewport is dirty - dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h}); - } else { - // Redraw dirty regions - if (v.x < c.x1) { - // left side dirty region - dirtyBoxes.push({'x': v.x, 'y': v.y, - 'w': c.x1 - v.x + 1, 'h': v.h}); - } - if (vx2 > c.x2) { - // right side dirty region - dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y, - 'w': vx2 - c.x2, 'h': v.h}); - } - if (v.y < c.y1) { - // top/middle dirty region - dirtyBoxes.push({'x': c.x1, 'y': v.y, - 'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y}); - } - if (vy2 > c.y2) { - // bottom/middle dirty region - dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1, - 'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2}); - } - } - - // Reset the cleanRect to the whole viewport - cleanRect = {'x1': v.x, 'y1': v.y, - 'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1}; - - return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; -}; - -// Translate viewport coordinates to absolute coordinates -that.absX = function(x) { - return x + viewport.x; -}; -that.absY = function(y) { - return y + viewport.y; -}; - - -that.resize = function(width, height) { - c_prevStyle = ""; - - fb_width = width; - fb_height = height; - - rescale(conf.scale); - that.viewportChange(); -}; - -that.clear = function() { - - if (conf.logo) { - that.resize(conf.logo.width, conf.logo.height); - that.blitStringImage(conf.logo.data, 0, 0); - } else { - that.resize(640, 20); - c_ctx.clearRect(0, 0, viewport.w, viewport.h); - } - - renderQ = []; - - // No benefit over default ("source-over") in Chrome and firefox - //c_ctx.globalCompositeOperation = "copy"; -}; - -that.fillRect = function(x, y, width, height, color) { - setFillColor(color); - c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height); -}; - -that.copyImage = function(old_x, old_y, new_x, new_y, w, h) { - var x1 = old_x - viewport.x, y1 = old_y - viewport.y, - x2 = new_x - viewport.x, y2 = new_y - viewport.y; - c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h); -}; - - -// Start updating a tile -that.startTile = function(x, y, width, height, color) { - var data, bgr, red, green, blue, i; - tile_x = x; - tile_y = y; - if ((width === 16) && (height === 16)) { - tile = tile16x16; - } else { - tile = c_ctx.createImageData(width, height); - } - data = tile.data; - if (conf.prefer_js) { - if (conf.true_color) { - bgr = color; - } else { - bgr = conf.colourMap[color[0]]; - } - red = bgr[2]; - green = bgr[1]; - blue = bgr[0]; - for (i = 0; i < (width * height * 4); i+=4) { - data[i ] = red; - data[i + 1] = green; - data[i + 2] = blue; - data[i + 3] = 255; - } - } else { - that.fillRect(x, y, width, height, color); - } -}; - -// Update sub-rectangle of the current tile -that.subTile = function(x, y, w, h, color) { - var data, p, bgr, red, green, blue, width, j, i, xend, yend; - if (conf.prefer_js) { - data = tile.data; - width = tile.width; - if (conf.true_color) { - bgr = color; - } else { - bgr = conf.colourMap[color[0]]; - } - red = bgr[2]; - green = bgr[1]; - blue = bgr[0]; - xend = x + w; - yend = y + h; - for (j = y; j < yend; j += 1) { - for (i = x; i < xend; i += 1) { - p = (i + (j * width) ) * 4; - data[p ] = red; - data[p + 1] = green; - data[p + 2] = blue; - data[p + 3] = 255; - } - } - } else { - that.fillRect(tile_x + x, tile_y + y, w, h, color); - } -}; - -// Draw the current tile to the screen -that.finishTile = function() { - if (conf.prefer_js) { - c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y); - } - // else: No-op, if not prefer_js then already done by setSubTile -}; - -rgbImageData = function(x, y, width, height, arr, offset) { - var img, i, j, data, v = viewport; - /* - if ((x - v.x >= v.w) || (y - v.y >= v.h) || - (x - v.x + width < 0) || (y - v.y + height < 0)) { - // Skipping because outside of viewport - return; - } - */ - img = c_ctx.createImageData(width, height); - data = img.data; - for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) { - data[i ] = arr[j ]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j + 2]; - data[i + 3] = 255; // Set Alpha - } - c_ctx.putImageData(img, x - v.x, y - v.y); -}; - -bgrxImageData = function(x, y, width, height, arr, offset) { - var img, i, j, data, v = viewport; - /* - if ((x - v.x >= v.w) || (y - v.y >= v.h) || - (x - v.x + width < 0) || (y - v.y + height < 0)) { - // Skipping because outside of viewport - return; - } - */ - img = c_ctx.createImageData(width, height); - data = img.data; - for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) { - data[i ] = arr[j + 2]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j ]; - data[i + 3] = 255; // Set Alpha - } - c_ctx.putImageData(img, x - v.x, y - v.y); -}; - -cmapImageData = function(x, y, width, height, arr, offset) { - var img, i, j, data, bgr, cmap; - img = c_ctx.createImageData(width, height); - data = img.data; - cmap = conf.colourMap; - for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) { - bgr = cmap[arr[j]]; - data[i ] = bgr[2]; - data[i + 1] = bgr[1]; - data[i + 2] = bgr[0]; - data[i + 3] = 255; // Set Alpha - } - c_ctx.putImageData(img, x - viewport.x, y - viewport.y); -}; - -that.blitImage = function(x, y, width, height, arr, offset) { - if (conf.true_color) { - bgrxImageData(x, y, width, height, arr, offset); - } else { - cmapImageData(x, y, width, height, arr, offset); - } -}; - -that.blitRgbImage = function(x, y, width, height, arr, offset) { - if (conf.true_color) { - rgbImageData(x, y, width, height, arr, offset); - } else { - // prolly wrong... - cmapImageData(x, y, width, height, arr, offset); - } -}; - -that.blitStringImage = function(str, x, y) { - var img = new Image(); - img.onload = function () { - c_ctx.drawImage(img, x - viewport.x, y - viewport.y); - }; - img.src = str; -}; - -// Wrap ctx.drawImage but relative to viewport -that.drawImage = function(img, x, y) { - c_ctx.drawImage(img, x - viewport.x, y - viewport.y); -}; - -that.renderQ_push = function(action) { - renderQ.push(action); - if (renderQ.length === 1) { - // If this can be rendered immediately it will be, otherwise - // the scanner will start polling the queue (every - // requestAnimationFrame interval) - scan_renderQ(); - } -}; - -scan_renderQ = function() { - var a, ready = true; - while (ready && renderQ.length > 0) { - a = renderQ[0]; - switch (a.type) { - case 'copy': - that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); - break; - case 'fill': - that.fillRect(a.x, a.y, a.width, a.height, a.color); - break; - case 'blit': - that.blitImage(a.x, a.y, a.width, a.height, a.data, 0); - break; - case 'blitRgb': - that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); - break; - case 'img': - if (a.img.complete) { - that.drawImage(a.img, a.x, a.y); + if (deltaX === 0 && deltaY === 0) { + return; + } + Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); + + vp.x += deltaX; + vx2 += deltaX; + vp.y += deltaY; + vy2 += deltaY; + + // Update the clean rectangle + if (vp.x > cr.x1) { + cr.x1 = vp.x; + } + if (vx2 < cr.x2) { + cr.x2 = vx2; + } + if (vp.y > cr.y1) { + cr.y1 = vp.y; + } + if (vy2 < cr.y2) { + cr.y2 = vy2; + } + + var x1, w; + if (deltaX < 0) { + // Shift viewport left, redraw left section + x1 = 0; + w = -deltaX; + } else { + // Shift viewport right, redraw right section + x1 = vp.w - deltaX; + w = deltaX; + } + + var y1, h; + if (deltaY < 0) { + // Shift viewport up, redraw top section + y1 = 0; + h = -deltaY; + } else { + // Shift viewport down, redraw bottom section + y1 = vp.h - deltaY; + h = deltaY; + } + + // Copy the valid part of the viewport to the shifted location + var saveStyle = this._drawCtx.fillStyle; + this._drawCtx.fillStyle = "rgb(255,255,255)"; + if (deltaX !== 0) { + this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h); + this._drawCtx.fillRect(x1, 0, w, vp.h); + } + if (deltaY !== 0) { + this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h); + this._drawCtx.fillRect(0, y1, vp.w, h); + } + this._drawCtx.fillStyle = saveStyle; + }, + + // Return a map of clean and dirty areas of the viewport and reset the + // tracking of clean and dirty areas + // + // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h}, + // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] } + getCleanDirtyReset: function () { + var vp = this._viewportLoc; + var cr = this._cleanRect; + + var cleanBox = { 'x': cr.x1, 'y': cr.y1, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 }; + + var dirtyBoxes = []; + if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) { + // Whole viewport is dirty + dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h }); + } else { + // Redraw dirty regions + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + + if (vp.x < cr.x1) { + // left side dirty region + dirtyBoxes.push({'x': vp.x, 'y': vp.y, + 'w': cr.x1 - vp.x + 1, 'h': vp.h}); + } + if (vx2 > cr.x2) { + // right side dirty region + dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y, + 'w': vx2 - cr.x2, 'h': vp.h}); + } + if(vp.y < cr.y1) { + // top/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': vp.y, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y}); + } + if (vy2 > cr.y2) { + // bottom/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1, + 'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2}); + } + } + + this._cleanRect = {'x1': vp.x, 'y1': vp.y, + 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1}; + + return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; + }, + + absX: function (x) { + return x + this._viewportLoc.x; + }, + + absY: function (y) { + return y + this._viewportLoc.y; + }, + + resize: function (width, height) { + this._prevDrawStyle = ""; + + this._fb_width = width; + this._fb_height = height; + + this._rescale(this._scale); + + this.viewportChange(); + }, + + clear: function () { + if (this._logo) { + this.resize(this._logo.width, this._logo.height); + this.blitStringImage(this._logo.data, 0, 0); + } else { + if (Util.Engine.trident === 6) { + // NB(directxman12): there's a bug in IE10 where we can fail to actually + // clear the canvas here because of the resize. + // Clearing the current viewport first fixes the issue + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } + this.resize(640, 20); + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } + + this._renderQ = []; + }, + + fillRect: function (x, y, width, height, color) { + this._setFillColor(color); + this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height); + }, + + copyImage: function (old_x, old_y, new_x, new_y, w, h) { + var x1 = old_x - this._viewportLoc.x; + var y1 = old_y - this._viewportLoc.y; + var x2 = new_x - this._viewportLoc.x; + var y2 = new_y - this._viewportLoc.y; + + this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h); + }, + + // start updating a tile + startTile: function (x, y, width, height, color) { + this._tile_x = x; + this._tile_y = y; + if (width === 16 && height === 16) { + this._tile = this._tile16x16; + } else { + this._tile = this._drawCtx.createImageData(width, height); + } + + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; } else { - // We need to wait for this image to 'load' - // to keep things in-order - ready = false; + bgr = this._colourMap[color[0]]; } - break; - } - if (ready) { - a = renderQ.shift(); - } - } - if (renderQ.length > 0) { - requestAnimFrame(scan_renderQ); - } -}; + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + + var data = this._tile.data; + for (var i = 0; i < width * height * 4; i += 4) { + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = 255; + } + } else { + this.fillRect(x, y, width, height, color); + } + }, + + // update sub-rectangle of the current tile + subTile: function (x, y, w, h, color) { + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + var xend = x + w; + var yend = y + h; + + var data = this._tile.data; + var width = this._tile.width; + for (var j = y; j < yend; j++) { + for (var i = x; i < xend; i++) { + var p = (i + (j * width)) * 4; + data[p] = red; + data[p + 1] = green; + data[p + 2] = blue; + data[p + 3] = 255; + } + } + } else { + this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color); + } + }, + // draw the current tile to the screen + finishTile: function () { + if (this._prefer_js) { + this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x, + this._tile_y - this._viewportLoc.y); + } + // else: No-op -- already done by setSubTile + }, -that.changeCursor = function(pixels, mask, hotx, hoty, w, h) { - if (conf.cursor_uri === false) { - Util.Warn("changeCursor called but no cursor data URI support"); - return; - } + blitImage: function (x, y, width, height, arr, offset) { + if (this._true_color) { + this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, - if (conf.true_color) { - changeCursor(conf.target, pixels, mask, hotx, hoty, w, h); - } else { - changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap); - } -}; + blitRgbImage: function (x, y , width, height, arr, offset) { + if (this._true_color) { + this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + // probably wrong? + this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, + + blitStringImage: function (str, x, y) { + var img = new Image(); + img.onload = function () { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }.bind(this); + img.src = str; + return img; // for debugging purposes + }, + + // wrap ctx.drawImage but relative to viewport + drawImage: function (img, x, y) { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }, + + renderQ_push: function (action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will start polling the queue (every + // requestAnimationFrame interval) + this._scan_renderQ(); + } + }, -that.defaultCursor = function() { - conf.target.style.cursor = "default"; -}; + changeCursor: function (pixels, mask, hotx, hoty, w, h) { + if (this._cursor_uri === false) { + Util.Warn("changeCursor called but no cursor data URI support"); + return; + } -return constructor(); // Return the public API interface + if (this._true_color) { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); + } else { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); + } + }, + + defaultCursor: function () { + this._target.style.cursor = "default"; + }, + + // Overridden getters/setters + get_context: function () { + return this._drawCtx; + }, + + set_scale: function (scale) { + this._rescale(scale); + }, + + set_width: function (w) { + this.resize(w, this._fb_height); + }, + get_width: function () { + return this._fb_width; + }, + + set_height: function (h) { + this.resize(this._fb_width, h); + }, + get_height: function () { + return this._fb_height; + }, + + // Private Methods + _rescale: function (factor) { + var canvas = this._target; + var properties = ['transform', 'WebkitTransform', 'MozTransform']; + var transform_prop; + while ((transform_prop = properties.shift())) { + if (typeof canvas.style[transform_prop] !== 'undefined') { + break; + } + } -} // End of Display() + if (transform_prop === null) { + Util.Debug("No scaling support"); + return; + } + if (typeof(factor) === "undefined") { + factor = this._scale; + } else if (factor > 1.0) { + factor = 1.0; + } else if (factor < 0.1) { + factor = 0.1; + } -/* Set CSS cursor property using data URI encoded cursor file */ -function changeCursor(target, pixels, mask, hotx, hoty, w, h, cmap) { - "use strict"; - var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y; - //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h); - - // Push multi-byte little-endian values - cur.push16le = function (num) { - this.push((num ) & 0xFF, - (num >> 8) & 0xFF ); - }; - cur.push32le = function (num) { - this.push((num ) & 0xFF, - (num >> 8) & 0xFF, - (num >> 16) & 0xFF, - (num >> 24) & 0xFF ); - }; + if (this._scale === factor) { + return; + } - IHDRsz = 40; - RGBsz = w * h * 4; - XORsz = Math.ceil( (w * h) / 8.0 ); - ANDsz = Math.ceil( (w * h) / 8.0 ); - - // Main header - cur.push16le(0); // 0: Reserved - cur.push16le(2); // 2: .CUR type - cur.push16le(1); // 4: Number of images, 1 for non-animated ico - - // Cursor #1 header (ICONDIRENTRY) - cur.push(w); // 6: width - cur.push(h); // 7: height - cur.push(0); // 8: colors, 0 -> true-color - cur.push(0); // 9: reserved - cur.push16le(hotx); // 10: hotspot x coordinate - cur.push16le(hoty); // 12: hotspot y coordinate - cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); - // 14: cursor data byte size - cur.push32le(22); // 18: offset of cursor data in the file - - - // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) - cur.push32le(IHDRsz); // 22: Infoheader size - cur.push32le(w); // 26: Cursor width - cur.push32le(h*2); // 30: XOR+AND height - cur.push16le(1); // 34: number of planes - cur.push16le(32); // 36: bits per pixel - cur.push32le(0); // 38: Type of compression - - cur.push32le(XORsz + ANDsz); // 43: Size of Image - // Gimp leaves this as 0 - - cur.push32le(0); // 46: reserved - cur.push32le(0); // 50: reserved - cur.push32le(0); // 54: reserved - cur.push32le(0); // 58: reserved - - // 62: color data (RGBQUAD icColors[]) - for (y = h-1; y >= 0; y -= 1) { - for (x = 0; x < w; x += 1) { - idx = y * Math.ceil(w / 8) + Math.floor(x/8); - alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; - - if (cmap) { - idx = (w * y) + x; - rgb = cmap[pixels[idx]]; - cur.push(rgb[2]); // blue - cur.push(rgb[1]); // green - cur.push(rgb[0]); // red - cur.push(alpha); // alpha + this._scale = factor; + var x = canvas.width - (canvas.width * factor); + var y = canvas.height - (canvas.height * factor); + canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)'; + }, + + _setFillColor: function (color) { + var bgr; + if (this._true_color) { + bgr = color; } else { - idx = ((w * y) + x) * 4; - cur.push(pixels[idx + 2]); // blue - cur.push(pixels[idx + 1]); // green - cur.push(pixels[idx ]); // red - cur.push(alpha); // alpha + bgr = this._colourMap[color[0]]; + } + + var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; + if (newStyle !== this._prevDrawStyle) { + this._drawCtx.fillStyle = newStyle; + this._prevDrawStyle = newStyle; + } + }, + + _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { + data[i] = arr[j]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { + data[i] = arr[j + 2]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + var cmap = this._colourMap; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { + var bgr = cmap[arr[j]]; + data[i] = bgr[2]; + data[i + 1] = bgr[1]; + data[i + 2] = bgr[0]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _scan_renderQ: function () { + var ready = true; + while (ready && this._renderQ.length > 0) { + var a = this._renderQ[0]; + switch (a.type) { + case 'copy': + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color); + break; + case 'blit': + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0); + break; + case 'blitRgb': + this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); + break; + case 'img': + if (a.img.complete) { + this.drawImage(a.img, a.x, a.y); + } else { + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + + if (ready) { + this._renderQ.shift(); + } } + + if (this._renderQ.length > 0) { + requestAnimFrame(this._scan_renderQ.bind(this)); + } + }, + }; + + Util.make_properties(Display, [ + ['target', 'wo', 'dom'], // Canvas element for rendering + ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only) + ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data} + ['true_color', 'rw', 'bool'], // Use true-color pixel data + ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) + ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0 + ['viewport', 'rw', 'bool'], // Use a viewport set with viewportChange() + ['width', 'rw', 'int'], // Display area width + ['height', 'rw', 'int'], // Display area height + + ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) + + ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods + ['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI + ]); + + // Class Methods + Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) { + var w = w0; + var h = h0; + if (h < w) { + h = w; // increase h to make it square + } else { + w = h; // increase w to make it square } - } - // XOR/bitmask data (BYTE icXOR[]) - // (ignored, just needs to be right size) - for (y = 0; y < h; y += 1) { - for (x = 0; x < Math.ceil(w / 8); x += 1) { - cur.push(0x00); + var cur = []; + + // Push multi-byte little-endian values + cur.push16le = function (num) { + this.push(num & 0xFF, (num >> 8) & 0xFF); + }; + cur.push32le = function (num) { + this.push(num & 0xFF, + (num >> 8) & 0xFF, + (num >> 16) & 0xFF, + (num >> 24) & 0xFF); + }; + + var IHDRsz = 40; + var RGBsz = w * h * 4; + var XORsz = Math.ceil((w * h) / 8.0); + var ANDsz = Math.ceil((w * h) / 8.0); + + cur.push16le(0); // 0: Reserved + cur.push16le(2); // 2: .CUR type + cur.push16le(1); // 4: Number of images, 1 for non-animated ico + + // Cursor #1 header (ICONDIRENTRY) + cur.push(w); // 6: width + cur.push(h); // 7: height + cur.push(0); // 8: colors, 0 -> true-color + cur.push(0); // 9: reserved + cur.push16le(hotx); // 10: hotspot x coordinate + cur.push16le(hoty); // 12: hotspot y coordinate + cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); + // 14: cursor data byte size + cur.push32le(22); // 18: offset of cursor data in the file + + // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) + cur.push32le(IHDRsz); // 22: InfoHeader size + cur.push32le(w); // 26: Cursor width + cur.push32le(h * 2); // 30: XOR+AND height + cur.push16le(1); // 34: number of planes + cur.push16le(32); // 36: bits per pixel + cur.push32le(0); // 38: Type of compression + + cur.push32le(XORsz + ANDsz); + // 42: Size of Image + cur.push32le(0); // 46: reserved + cur.push32le(0); // 50: reserved + cur.push32le(0); // 54: reserved + cur.push32le(0); // 58: reserved + + // 62: color data (RGBQUAD icColors[]) + var y, x; + for (y = h - 1; y >= 0; y--) { + for (x = 0; x < w; x++) { + if (x >= w0 || y >= h0) { + cur.push(0); // blue + cur.push(0); // green + cur.push(0); // red + cur.push(0); // alpha + } else { + var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); + var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + if (cmap) { + idx = (w0 * y) + x; + var rgb = cmap[pixels[idx]]; + cur.push(rgb[2]); // blue + cur.push(rgb[1]); // green + cur.push(rgb[0]); // red + cur.push(alpha); // alpha + } + } + } + } + + // XOR/bitmask data (BYTE icXOR[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } } - } - // AND/bitmask data (BYTE icAND[]) - // (ignored, just needs to be right size) - for (y = 0; y < h; y += 1) { - for (x = 0; x < Math.ceil(w / 8); x += 1) { - cur.push(0x00); + // AND/bitmask data (BYTE icAND[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } } - } - url = "data:image/x-icon;base64," + Base64.encode(cur); - target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; - //Util.Debug("<< changeCursor, cur.length: " + cur.length); -} + var url = 'data:image/x-icon;base64,' + Base64.encode(cur); + target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; + }; +})(); diff --git a/webclients/novnc/include/input.js b/webclients/novnc/include/input.js index 9298dfe..5d9e209 100644 --- a/webclients/novnc/include/input.js +++ b/webclients/novnc/include/input.js @@ -1,1911 +1,388 @@ /* * noVNC: HTML5 VNC client - * Copyright (C) 2011 Joel Martin - * Licensed under LGPL-2 or any later version (see LICENSE.txt) + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) */ -/*jslint browser: true, white: false, bitwise: false */ +/*jslint browser: true, white: false */ /*global window, Util */ +var Keyboard, Mouse; + +(function () { + "use strict"; + + // + // Keyboard event handler + // + + Keyboard = function (defaults) { + this._keyDownList = []; // List of depressed keys + // (even if they are happy) + + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true + }); + + // create the keyboard handler + this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(), + VerifyCharModifier( /* jshint newcap: false */ + TrackKeyState( + EscapeModifiers(this._handleRfbEvent.bind(this)) + ) + ) + ); /* jshint newcap: true */ + + // keep these here so we can refer to them later + this._eventHandlers = { + 'keyup': this._handleKeyUp.bind(this), + 'keydown': this._handleKeyDown.bind(this), + 'keypress': this._handleKeyPress.bind(this), + 'blur': this._allKeysUp.bind(this) + }; + }; + + Keyboard.prototype = { + // private methods + + _handleRfbEvent: function (e) { + if (this._onKeyPress) { + Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + + ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); + this._onKeyPress(e.keysym.keysym, e.type == 'keydown'); + } + }, -// -// Keyboard event handler -// + _handleKeyDown: function (e) { + if (!this._focused) { return true; } -function Keyboard(defaults) { -"use strict"; + if (this._handler.keydown(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, -var that = {}, // Public API methods - conf = {}, // Configuration attributes + _handleKeyPress: function (e) { + if (!this._focused) { return true; } - keyDownList = []; // List of depressed keys - // (even if they are happy) + if (this._handler.keypress(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'wo', 'dom', document, 'DOM element that captures keyboard input'], - ['focused', 'rw', 'bool', true, 'Capture and send key events'], + _handleKeyUp: function (e) { + if (!this._focused) { return true; } - ['onKeyPress', 'rw', 'func', null, 'Handler for key press/release'] - ]); + if (this._handler.keyup(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + _allKeysUp: function () { + Util.Debug(">> Keyboard.allKeysUp"); + this._handler.releaseAll(); + Util.Debug("<< Keyboard.allKeysUp"); + }, -// -// Private functions -// - -// From the event keyCode return the keysym value for keys that need -// to be suppressed otherwise they may trigger unintended browser -// actions -function getKeysymSpecial(evt) { - var keysym = null; - - switch ( evt.keyCode ) { - // These generate a keyDown and keyPress in Firefox and Opera - case 8 : keysym = 0xFF08; break; // BACKSPACE - case 13 : keysym = 0xFF0D; break; // ENTER - - // This generates a keyDown and keyPress in Opera - case 9 : keysym = 0xFF09; break; // TAB - default : break; - } - - if (evt.type === 'keydown') { - switch ( evt.keyCode ) { - case 27 : keysym = 0xFF1B; break; // ESCAPE - case 46 : keysym = 0xFFFF; break; // DELETE - - case 36 : keysym = 0xFF50; break; // HOME - case 35 : keysym = 0xFF57; break; // END - case 33 : keysym = 0xFF55; break; // PAGE_UP - case 34 : keysym = 0xFF56; break; // PAGE_DOWN - case 45 : keysym = 0xFF63; break; // INSERT - // '-' during keyPress - case 37 : keysym = 0xFF51; break; // LEFT - case 38 : keysym = 0xFF52; break; // UP - case 39 : keysym = 0xFF53; break; // RIGHT - case 40 : keysym = 0xFF54; break; // DOWN - case 16 : keysym = 0xFFE1; break; // SHIFT - case 17 : keysym = 0xFFE3; break; // CONTROL - //case 18 : keysym = 0xFFE7; break; // Left Meta (Mac Option) - case 18 : keysym = 0xFFE9; break; // Left ALT (Mac Command) - - case 112 : keysym = 0xFFBE; break; // F1 - case 113 : keysym = 0xFFBF; break; // F2 - case 114 : keysym = 0xFFC0; break; // F3 - case 115 : keysym = 0xFFC1; break; // F4 - case 116 : keysym = 0xFFC2; break; // F5 - case 117 : keysym = 0xFFC3; break; // F6 - case 118 : keysym = 0xFFC4; break; // F7 - case 119 : keysym = 0xFFC5; break; // F8 - case 120 : keysym = 0xFFC6; break; // F9 - case 121 : keysym = 0xFFC7; break; // F10 - case 122 : keysym = 0xFFC8; break; // F11 - case 123 : keysym = 0xFFC9; break; // F12 - - default : break; - } - } - - if ((!keysym) && (evt.ctrlKey || evt.altKey)) { - if ((typeof(evt.which) !== "undefined") && (evt.which > 0)) { - keysym = evt.which; - } else { - // IE9 always - // Firefox and Opera when ctrl/alt + special - Util.Warn("which not set, using keyCode"); - keysym = evt.keyCode; - } + // Public methods + + grab: function () { + //Util.Debug(">> Keyboard.grab"); + var c = this._target; + + Util.addEvent(c, 'keydown', this._eventHandlers.keydown); + Util.addEvent(c, 'keyup', this._eventHandlers.keyup); + Util.addEvent(c, 'keypress', this._eventHandlers.keypress); + + // Release (key up) if window loses focus + Util.addEvent(window, 'blur', this._eventHandlers.blur); + + //Util.Debug("<< Keyboard.grab"); + }, - /* Remap symbols */ - switch (keysym) { - case 186 : keysym = 59; break; // ; (IE) - case 187 : keysym = 61; break; // = (IE) - case 188 : keysym = 44; break; // , (Mozilla, IE) - case 109 : // - (Mozilla, Opera) - if (Util.Engine.gecko || Util.Engine.presto) { - keysym = 45; } - break; - case 189 : keysym = 45; break; // - (IE) - case 190 : keysym = 46; break; // . (Mozilla, IE) - case 191 : keysym = 47; break; // / (Mozilla, IE) - case 192 : keysym = 96; break; // ` (Mozilla, IE) - case 219 : keysym = 91; break; // [ (Mozilla, IE) - case 220 : keysym = 92; break; // \ (Mozilla, IE) - case 221 : keysym = 93; break; // ] (Mozilla, IE) - case 222 : keysym = 39; break; // ' (Mozilla, IE) + ungrab: function () { + //Util.Debug(">> Keyboard.ungrab"); + var c = this._target; + + Util.removeEvent(c, 'keydown', this._eventHandlers.keydown); + Util.removeEvent(c, 'keyup', this._eventHandlers.keyup); + Util.removeEvent(c, 'keypress', this._eventHandlers.keypress); + Util.removeEvent(window, 'blur', this._eventHandlers.blur); + + // Release (key up) all keys that are in a down state + this._allKeysUp(); + + //Util.Debug(">> Keyboard.ungrab"); + }, + + sync: function (e) { + this._handler.syncModifiers(e); } - - /* Remap shifted and unshifted keys */ - if (!!evt.shiftKey) { - switch (keysym) { - case 48 : keysym = 41 ; break; // ) (shifted 0) - case 49 : keysym = 33 ; break; // ! (shifted 1) - case 50 : keysym = 64 ; break; // @ (shifted 2) - case 51 : keysym = 35 ; break; // # (shifted 3) - case 52 : keysym = 36 ; break; // $ (shifted 4) - case 53 : keysym = 37 ; break; // % (shifted 5) - case 54 : keysym = 94 ; break; // ^ (shifted 6) - case 55 : keysym = 38 ; break; // & (shifted 7) - case 56 : keysym = 42 ; break; // * (shifted 8) - case 57 : keysym = 40 ; break; // ( (shifted 9) - - case 59 : keysym = 58 ; break; // : (shifted `) - case 61 : keysym = 43 ; break; // + (shifted ;) - case 44 : keysym = 60 ; break; // < (shifted ,) - case 45 : keysym = 95 ; break; // _ (shifted -) - case 46 : keysym = 62 ; break; // > (shifted .) - case 47 : keysym = 63 ; break; // ? (shifted /) - case 96 : keysym = 126; break; // ~ (shifted `) - case 91 : keysym = 123; break; // { (shifted [) - case 92 : keysym = 124; break; // | (shifted \) - case 93 : keysym = 125; break; // } (shifted ]) - case 39 : keysym = 34 ; break; // " (shifted ') + }; + + Util.make_properties(Keyboard, [ + ['target', 'wo', 'dom'], // DOM element that captures keyboard input + ['focused', 'rw', 'bool'], // Capture and send key events + + ['onKeyPress', 'rw', 'func'] // Handler for key press/release + ]); + + // + // Mouse event handler + // + + Mouse = function (defaults) { + this._mouseCaptured = false; + + this._doubleClickTimer = null; + this._lastTouchPos = null; + + // Configuration attributes + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true, + 'scale': 1.0, + 'touchButton': 1 + }); + + this._eventHandlers = { + 'mousedown': this._handleMouseDown.bind(this), + 'mouseup': this._handleMouseUp.bind(this), + 'mousemove': this._handleMouseMove.bind(this), + 'mousewheel': this._handleMouseWheel.bind(this), + 'mousedisable': this._handleMouseDisable.bind(this) + }; + }; + + Mouse.prototype = { + // private methods + _captureMouse: function () { + // capturing the mouse ensures we get the mouseup event + if (this._target.setCapture) { + this._target.setCapture(); } - } else if ((keysym >= 65) && (keysym <=90)) { - /* Remap unshifted A-Z */ - keysym += 32; - } else if (evt.keyLocation === 3) { - // numpad keys - switch (keysym) { - case 96 : keysym = 48; break; // 0 - case 97 : keysym = 49; break; // 1 - case 98 : keysym = 50; break; // 2 - case 99 : keysym = 51; break; // 3 - case 100: keysym = 52; break; // 4 - case 101: keysym = 53; break; // 5 - case 102: keysym = 54; break; // 6 - case 103: keysym = 55; break; // 7 - case 104: keysym = 56; break; // 8 - case 105: keysym = 57; break; // 9 - case 109: keysym = 45; break; // - - case 110: keysym = 46; break; // . - case 111: keysym = 47; break; // / + + // some browsers give us mouseup events regardless, + // so if we never captured the mouse, we can disregard the event + this._mouseCaptured = true; + }, + + _releaseMouse: function () { + if (this._target.releaseCapture) { + this._target.releaseCapture(); } - } - } - - return keysym; -} - -/* Translate DOM keyPress event to keysym value */ -function getKeysym(evt) { - var keysym, msg; - - if (typeof(evt.which) !== "undefined") { - // WebKit, Firefox, Opera - keysym = evt.which; - } else { - // IE9 - Util.Warn("which not set, using keyCode"); - keysym = evt.keyCode; - } - - if ((keysym > 255) && (keysym < 0xFF00)) { - msg = "Mapping character code " + keysym; - // Map Unicode outside Latin 1 to X11 keysyms - keysym = unicodeTable[keysym]; - if (typeof(keysym) === 'undefined') { - keysym = 0; - } - Util.Debug(msg + " to " + keysym); - } - - return keysym; -} - -function show_keyDownList(kind) { - var c; - var msg = "keyDownList (" + kind + "):\n"; - for (c = 0; c < keyDownList.length; c++) { - msg = msg + " " + c + " - keyCode: " + keyDownList[c].keyCode + - " - which: " + keyDownList[c].which + "\n"; - } - Util.Debug(msg); -} - -function copyKeyEvent(evt) { - var members = ['type', 'keyCode', 'charCode', 'which', - 'altKey', 'ctrlKey', 'shiftKey', - 'keyLocation', 'keyIdentifier'], i, obj = {}; - for (i = 0; i < members.length; i++) { - if (typeof(evt[members[i]]) !== "undefined") { - obj[members[i]] = evt[members[i]]; - } - } - return obj; -} - -function pushKeyEvent(fevt) { - keyDownList.push(fevt); -} - -function getKeyEvent(keyCode, pop) { - var i, fevt = null; - for (i = keyDownList.length-1; i >= 0; i--) { - if (keyDownList[i].keyCode === keyCode) { - if ((typeof(pop) !== "undefined") && (pop)) { - fevt = keyDownList.splice(i, 1)[0]; + this._mouseCaptured = false; + }, + + _resetDoubleClickTimer: function () { + this._doubleClickTimer = null; + }, + + _handleMouseButton: function (e, down) { + if (!this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + var bmask; + if (e.touches || e.changedTouches) { + // Touch device + + // When two touches occur within 500 ms of each other and are + // closer than 20 pixels together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + var xs = this._lastTouchPos.x - pos.x; + var ys = this._lastTouchPos.y - pos.y; + var d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + if (d < 20 * window.devicePixelRatio) { + pos = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this._touchButton; + // If bmask is set + } else if (evt.which) { + /* everything except IE */ + bmask = 1 << evt.button; } else { - fevt = keyDownList[i]; + /* IE including 9 */ + bmask = (evt.button & 0x1) + // Left + (evt.button & 0x2) * 2 + // Right + (evt.button & 0x4) / 2; // Middle } - break; - } - } - return fevt; -} - -function ignoreKeyEvent(evt) { - // Blarg. Some keys have a different keyCode on keyDown vs keyUp - if (evt.keyCode === 229) { - // French AZERTY keyboard dead key. - // Lame thing is that the respective keyUp is 219 so we can't - // properly ignore the keyUp event - return true; - } - return false; -} - - -// -// Key Event Handling: -// -// There are several challenges when dealing with key events: -// - The meaning and use of keyCode, charCode and which depends on -// both the browser and the event type (keyDown/Up vs keyPress). -// - We cannot automatically determine the keyboard layout -// - The keyDown and keyUp events have a keyCode value that has not -// been translated by modifier keys. -// - The keyPress event has a translated (for layout and modifiers) -// character code but the attribute containing it differs. keyCode -// contains the translated value in WebKit (Chrome/Safari), Opera -// 11 and IE9. charCode contains the value in WebKit and Firefox. -// The which attribute contains the value on WebKit, Firefox and -// Opera 11. -// - The keyDown/Up keyCode value indicates (sort of) the physical -// key was pressed but only for standard US layout. On a US -// keyboard, the '-' and '_' characters are on the same key and -// generate a keyCode value of 189. But on an AZERTY keyboard even -// though they are different physical keys they both still -// generate a keyCode of 189! -// - To prevent a key event from propagating to the browser and -// causing unwanted default actions (such as closing a tab, -// opening a menu, shifting focus, etc) we must suppress this -// event in both keyDown and keyPress because not all key strokes -// generate on a keyPress event. Also, in WebKit and IE9 -// suppressing the keyDown prevents a keyPress but other browsers -// still generated a keyPress even if keyDown is suppressed. -// -// For safe key events, we wait until the keyPress event before -// reporting a key down event. For unsafe key events, we report a key -// down event when the keyDown event fires and we suppress any further -// actions (including keyPress). -// -// In order to report a key up event that matches what we reported -// for the key down event, we keep a list of keys that are currently -// down. When the keyDown event happens, we add the key event to the -// list. If it is a safe key event, then we update the which attribute -// in the most recent item on the list when we received a keyPress -// event (keyPress should immediately follow keyDown). When we -// received a keyUp event we search for the event on the list with -// a matching keyCode and we report the character code using the value -// in the 'which' attribute that was stored with that key. -// - -function onKeyDown(e) { - if (! conf.focused) { - return true; - } - var fevt = null, evt = (e ? e : window.event), - keysym = null, suppress = false; - //Util.Debug("onKeyDown kC:" + evt.keyCode + " cC:" + evt.charCode + " w:" + evt.which); - - fevt = copyKeyEvent(evt); - - keysym = getKeysymSpecial(evt); - // Save keysym decoding for use in keyUp - fevt.keysym = keysym; - if (keysym) { - // If it is a key or key combination that might trigger - // browser behaviors or it has no corresponding keyPress - // event, then send it immediately - if (conf.onKeyPress && !ignoreKeyEvent(evt)) { - Util.Debug("onKeyPress down, keysym: " + keysym + - " (onKeyDown key: " + evt.keyCode + - ", which: " + evt.which + ")"); - conf.onKeyPress(keysym, 1, evt); - } - suppress = true; - } - - if (! ignoreKeyEvent(evt)) { - // Add it to the list of depressed keys - pushKeyEvent(fevt); - //show_keyDownList('down'); - } - - if (suppress) { - // Suppress bubbling/default actions - Util.stopEvent(e); - return false; - } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated - return true; - } -} - -function onKeyPress(e) { - if (! conf.focused) { - return true; - } - var evt = (e ? e : window.event), - kdlen = keyDownList.length, keysym = null; - //Util.Debug("onKeyPress kC:" + evt.keyCode + " cC:" + evt.charCode + " w:" + evt.which); - - if (((evt.which !== "undefined") && (evt.which === 0)) || - (getKeysymSpecial(evt))) { - // Firefox and Opera generate a keyPress event even if keyDown - // is suppressed. But the keys we want to suppress will have - // either: - // - the which attribute set to 0 - // - getKeysymSpecial() will identify it - Util.Debug("Ignoring special key in keyPress"); - Util.stopEvent(e); - return false; - } - - keysym = getKeysym(evt); - - // Modify the the which attribute in the depressed keys list so - // that the keyUp event will be able to have the character code - // translation available. - if (kdlen > 0) { - keyDownList[kdlen-1].keysym = keysym; - } else { - Util.Warn("keyDownList empty when keyPress triggered"); - } - - //show_keyDownList('press'); - - // Send the translated keysym - if (conf.onKeyPress && (keysym > 0)) { - Util.Debug("onKeyPress down, keysym: " + keysym + - " (onKeyPress key: " + evt.keyCode + - ", which: " + evt.which + ")"); - conf.onKeyPress(keysym, 1, evt); - } - - // Stop keypress events just in case - Util.stopEvent(e); - return false; -} - -function onKeyUp(e) { - if (! conf.focused) { - return true; - } - var fevt = null, evt = (e ? e : window.event), keysym; - //Util.Debug("onKeyUp kC:" + evt.keyCode + " cC:" + evt.charCode + " w:" + evt.which); - - fevt = getKeyEvent(evt.keyCode, true); - - if (fevt) { - keysym = fevt.keysym; - } else { - Util.Warn("Key event (keyCode = " + evt.keyCode + - ") not found on keyDownList"); - keysym = 0; - } - - //show_keyDownList('up'); - - if (conf.onKeyPress && (keysym > 0)) { - //Util.Debug("keyPress up, keysym: " + keysym + - // " (key: " + evt.keyCode + ", which: " + evt.which + ")"); - Util.Debug("onKeyPress up, keysym: " + keysym + - " (onKeyPress key: " + evt.keyCode + - ", which: " + evt.which + ")"); - conf.onKeyPress(keysym, 0, evt); - } - Util.stopEvent(e); - return false; -} - -function allKeysUp() { - Util.Debug(">> Keyboard.allKeysUp"); - if (keyDownList.length > 0) { - Util.Info("Releasing pressed/down keys"); - } - var i, keysym, fevt = null; - for (i = keyDownList.length-1; i >= 0; i--) { - fevt = keyDownList.splice(i, 1)[0]; - keysym = fevt.keysym; - if (conf.onKeyPress && (keysym > 0)) { - Util.Debug("allKeysUp, keysym: " + keysym + - " (keyCode: " + fevt.keyCode + - ", which: " + fevt.which + ")"); - conf.onKeyPress(keysym, 0, fevt); - } - } - Util.Debug("<< Keyboard.allKeysUp"); - return; -} -// -// Public API interface functions -// + if (this._onMouseButton) { + Util.Debug("onMouseButton " + (down ? "down" : "up") + + ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); + this._onMouseButton(pos.x, pos.y, down, bmask); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDown: function (e) { + this._captureMouse(); + this._handleMouseButton(e, 1); + }, -that.grab = function() { - //Util.Debug(">> Keyboard.grab"); - var c = conf.target; + _handleMouseUp: function (e) { + if (!this._mouseCaptured) { return; } - Util.addEvent(c, 'keydown', onKeyDown); - Util.addEvent(c, 'keyup', onKeyUp); - Util.addEvent(c, 'keypress', onKeyPress); + this._handleMouseButton(e, 0); + this._releaseMouse(); + }, - // Release (key up) if window loses focus - Util.addEvent(window, 'blur', allKeysUp); + _handleMouseWheel: function (e) { + if (!this._focused) { return true; } - //Util.Debug("<< Keyboard.grab"); -}; + if (this._notify) { + this._notify(e); + } -that.ungrab = function() { - //Util.Debug(">> Keyboard.ungrab"); - var c = conf.target; + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; + var bmask; + if (wheelData > 0) { + bmask = 1 << 3; + } else { + bmask = 1 << 4; + } - Util.removeEvent(c, 'keydown', onKeyDown); - Util.removeEvent(c, 'keyup', onKeyUp); - Util.removeEvent(c, 'keypress', onKeyPress); - Util.removeEvent(window, 'blur', allKeysUp); + if (this._onMouseButton) { + this._onMouseButton(pos.x, pos.y, 1, bmask); + this._onMouseButton(pos.x, pos.y, 0, bmask); + } + Util.stopEvent(e); + return false; + }, - // Release (key up) all keys that are in a down state - allKeysUp(); + _handleMouseMove: function (e) { + if (! this._focused) { return true; } - //Util.Debug(">> Keyboard.ungrab"); -}; + if (this._notify) { + this._notify(e); + } -return that; // Return the public API interface + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + if (this._onMouseMove) { + this._onMouseMove(pos.x, pos.y); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDisable: function (e) { + if (!this._focused) { return true; } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + /* Stop propagation if inside canvas area */ + if ((pos.realx >= 0) && (pos.realy >= 0) && + (pos.realx < this._target.offsetWidth) && + (pos.realy < this._target.offsetHeight)) { + //Util.Debug("mouse event disabled"); + Util.stopEvent(e); + return false; + } -} // End of Keyboard() + return true; + }, -// -// Mouse event handler -// + // Public methods + grab: function () { + var c = this._target; -function Mouse(defaults) { -"use strict"; + if ('ontouchstart' in document.documentElement) { + Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.addEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } -var that = {}, // Public API methods - conf = {}; // Configuration attributes + /* Work around right and middle click browser behaviors */ + Util.addEvent(document, 'click', this._eventHandlers.mousedisable); + Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + }, -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'ro', 'dom', document, 'DOM element that captures mouse input'], - ['focused', 'rw', 'bool', true, 'Capture and send mouse clicks/movement'], - ['scale', 'rw', 'float', 1.0, 'Viewport scale factor 0.0 - 1.0'], + ungrab: function () { + var c = this._target; - ['onMouseButton', 'rw', 'func', null, 'Handler for mouse button click/release'], - ['onMouseMove', 'rw', 'func', null, 'Handler for mouse movement'], - ['touchButton', 'rw', 'int', 1, 'Button mask (1, 2, 4) for touch devices (0 means ignore clicks)'] - ]); + if ('ontouchstart' in document.documentElement) { + Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } + /* Work around right and middle click browser behaviors */ + Util.removeEvent(document, 'click', this._eventHandlers.mousedisable); + Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); -// -// Private functions -// - -function onMouseButton(e, down) { - var evt, pos, bmask; - if (! conf.focused) { - return true; - } - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - if (e.touches || e.changedTouches) { - // Touch device - bmask = conf.touchButton; - // If bmask is set - } else if (evt.which) { - /* everything except IE */ - bmask = 1 << evt.button; - } else { - /* IE including 9 */ - bmask = (evt.button & 0x1) + // Left - (evt.button & 0x2) * 2 + // Right - (evt.button & 0x4) / 2; // Middle - } - //Util.Debug("mouse " + pos.x + "," + pos.y + " down: " + down + - // " bmask: " + bmask + "(evt.button: " + evt.button + ")"); - if (bmask > 0 && conf.onMouseButton) { - Util.Debug("onMouseButton " + (down ? "down" : "up") + - ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); - conf.onMouseButton(pos.x, pos.y, down, bmask); - } - Util.stopEvent(e); - return false; -} - -function onMouseDown(e) { - onMouseButton(e, 1); -} - -function onMouseUp(e) { - onMouseButton(e, 0); -} - -function onMouseWheel(e) { - var evt, pos, bmask, wheelData; - if (! conf.focused) { - return true; - } - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; - if (wheelData > 0) { - bmask = 1 << 3; - } else { - bmask = 1 << 4; - } - //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y); - if (conf.onMouseButton) { - conf.onMouseButton(pos.x, pos.y, 1, bmask); - conf.onMouseButton(pos.x, pos.y, 0, bmask); - } - Util.stopEvent(e); - return false; -} - -function onMouseMove(e) { - var evt, pos; - if (! conf.focused) { - return true; - } - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y); - if (conf.onMouseMove) { - conf.onMouseMove(pos.x, pos.y); - } - Util.stopEvent(e); - return false; -} - -function onMouseDisable(e) { - var evt, pos; - if (! conf.focused) { - return true; - } - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - /* Stop propagation if inside canvas area */ - if ((pos.x >= 0) && (pos.y >= 0) && - (pos.x < conf.target.offsetWidth) && - (pos.y < conf.target.offsetHeight)) { - //Util.Debug("mouse event disabled"); - Util.stopEvent(e); - return false; - } - //Util.Debug("mouse event not disabled"); - return true; -} - -// -// Public API interface functions -// - -that.grab = function() { - //Util.Debug(">> Mouse.grab"); - var c = conf.target; - - if ('ontouchstart' in document.documentElement) { - Util.addEvent(c, 'touchstart', onMouseDown); - Util.addEvent(c, 'touchend', onMouseUp); - Util.addEvent(c, 'touchmove', onMouseMove); - } else { - Util.addEvent(c, 'mousedown', onMouseDown); - Util.addEvent(c, 'mouseup', onMouseUp); - Util.addEvent(c, 'mousemove', onMouseMove); - Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', - onMouseWheel); - } - - /* Work around right and middle click browser behaviors */ - Util.addEvent(document, 'click', onMouseDisable); - Util.addEvent(document.body, 'contextmenu', onMouseDisable); - - //Util.Debug("<< Mouse.grab"); -}; - -that.ungrab = function() { - //Util.Debug(">> Mouse.ungrab"); - var c = conf.target; - - if ('ontouchstart' in document.documentElement) { - Util.removeEvent(c, 'touchstart', onMouseDown); - Util.removeEvent(c, 'touchend', onMouseUp); - Util.removeEvent(c, 'touchmove', onMouseMove); - } else { - Util.removeEvent(c, 'mousedown', onMouseDown); - Util.removeEvent(c, 'mouseup', onMouseUp); - Util.removeEvent(c, 'mousemove', onMouseMove); - Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', - onMouseWheel); - } - - /* Work around right and middle click browser behaviors */ - Util.removeEvent(document, 'click', onMouseDisable); - Util.removeEvent(document.body, 'contextmenu', onMouseDisable); - - //Util.Debug(">> Mouse.ungrab"); -}; - -return that; // Return the public API interface - -} // End of Mouse() + } + }; + Util.make_properties(Mouse, [ + ['target', 'ro', 'dom'], // DOM element that captures mouse input + ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received + ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement + ['scale', 'rw', 'float'], // Viewport scale factor 0.0 - 1.0 -/* - * Browser keypress to X11 keysym for Unicode characters > U+00FF - */ -unicodeTable = { - 0x0104 : 0x01a1, - 0x02D8 : 0x01a2, - 0x0141 : 0x01a3, - 0x013D : 0x01a5, - 0x015A : 0x01a6, - 0x0160 : 0x01a9, - 0x015E : 0x01aa, - 0x0164 : 0x01ab, - 0x0179 : 0x01ac, - 0x017D : 0x01ae, - 0x017B : 0x01af, - 0x0105 : 0x01b1, - 0x02DB : 0x01b2, - 0x0142 : 0x01b3, - 0x013E : 0x01b5, - 0x015B : 0x01b6, - 0x02C7 : 0x01b7, - 0x0161 : 0x01b9, - 0x015F : 0x01ba, - 0x0165 : 0x01bb, - 0x017A : 0x01bc, - 0x02DD : 0x01bd, - 0x017E : 0x01be, - 0x017C : 0x01bf, - 0x0154 : 0x01c0, - 0x0102 : 0x01c3, - 0x0139 : 0x01c5, - 0x0106 : 0x01c6, - 0x010C : 0x01c8, - 0x0118 : 0x01ca, - 0x011A : 0x01cc, - 0x010E : 0x01cf, - 0x0110 : 0x01d0, - 0x0143 : 0x01d1, - 0x0147 : 0x01d2, - 0x0150 : 0x01d5, - 0x0158 : 0x01d8, - 0x016E : 0x01d9, - 0x0170 : 0x01db, - 0x0162 : 0x01de, - 0x0155 : 0x01e0, - 0x0103 : 0x01e3, - 0x013A : 0x01e5, - 0x0107 : 0x01e6, - 0x010D : 0x01e8, - 0x0119 : 0x01ea, - 0x011B : 0x01ec, - 0x010F : 0x01ef, - 0x0111 : 0x01f0, - 0x0144 : 0x01f1, - 0x0148 : 0x01f2, - 0x0151 : 0x01f5, - 0x0171 : 0x01fb, - 0x0159 : 0x01f8, - 0x016F : 0x01f9, - 0x0163 : 0x01fe, - 0x02D9 : 0x01ff, - 0x0126 : 0x02a1, - 0x0124 : 0x02a6, - 0x0130 : 0x02a9, - 0x011E : 0x02ab, - 0x0134 : 0x02ac, - 0x0127 : 0x02b1, - 0x0125 : 0x02b6, - 0x0131 : 0x02b9, - 0x011F : 0x02bb, - 0x0135 : 0x02bc, - 0x010A : 0x02c5, - 0x0108 : 0x02c6, - 0x0120 : 0x02d5, - 0x011C : 0x02d8, - 0x016C : 0x02dd, - 0x015C : 0x02de, - 0x010B : 0x02e5, - 0x0109 : 0x02e6, - 0x0121 : 0x02f5, - 0x011D : 0x02f8, - 0x016D : 0x02fd, - 0x015D : 0x02fe, - 0x0138 : 0x03a2, - 0x0156 : 0x03a3, - 0x0128 : 0x03a5, - 0x013B : 0x03a6, - 0x0112 : 0x03aa, - 0x0122 : 0x03ab, - 0x0166 : 0x03ac, - 0x0157 : 0x03b3, - 0x0129 : 0x03b5, - 0x013C : 0x03b6, - 0x0113 : 0x03ba, - 0x0123 : 0x03bb, - 0x0167 : 0x03bc, - 0x014A : 0x03bd, - 0x014B : 0x03bf, - 0x0100 : 0x03c0, - 0x012E : 0x03c7, - 0x0116 : 0x03cc, - 0x012A : 0x03cf, - 0x0145 : 0x03d1, - 0x014C : 0x03d2, - 0x0136 : 0x03d3, - 0x0172 : 0x03d9, - 0x0168 : 0x03dd, - 0x016A : 0x03de, - 0x0101 : 0x03e0, - 0x012F : 0x03e7, - 0x0117 : 0x03ec, - 0x012B : 0x03ef, - 0x0146 : 0x03f1, - 0x014D : 0x03f2, - 0x0137 : 0x03f3, - 0x0173 : 0x03f9, - 0x0169 : 0x03fd, - 0x016B : 0x03fe, - 0x1E02 : 0x1001e02, - 0x1E03 : 0x1001e03, - 0x1E0A : 0x1001e0a, - 0x1E80 : 0x1001e80, - 0x1E82 : 0x1001e82, - 0x1E0B : 0x1001e0b, - 0x1EF2 : 0x1001ef2, - 0x1E1E : 0x1001e1e, - 0x1E1F : 0x1001e1f, - 0x1E40 : 0x1001e40, - 0x1E41 : 0x1001e41, - 0x1E56 : 0x1001e56, - 0x1E81 : 0x1001e81, - 0x1E57 : 0x1001e57, - 0x1E83 : 0x1001e83, - 0x1E60 : 0x1001e60, - 0x1EF3 : 0x1001ef3, - 0x1E84 : 0x1001e84, - 0x1E85 : 0x1001e85, - 0x1E61 : 0x1001e61, - 0x0174 : 0x1000174, - 0x1E6A : 0x1001e6a, - 0x0176 : 0x1000176, - 0x0175 : 0x1000175, - 0x1E6B : 0x1001e6b, - 0x0177 : 0x1000177, - 0x0152 : 0x13bc, - 0x0153 : 0x13bd, - 0x0178 : 0x13be, - 0x203E : 0x047e, - 0x3002 : 0x04a1, - 0x300C : 0x04a2, - 0x300D : 0x04a3, - 0x3001 : 0x04a4, - 0x30FB : 0x04a5, - 0x30F2 : 0x04a6, - 0x30A1 : 0x04a7, - 0x30A3 : 0x04a8, - 0x30A5 : 0x04a9, - 0x30A7 : 0x04aa, - 0x30A9 : 0x04ab, - 0x30E3 : 0x04ac, - 0x30E5 : 0x04ad, - 0x30E7 : 0x04ae, - 0x30C3 : 0x04af, - 0x30FC : 0x04b0, - 0x30A2 : 0x04b1, - 0x30A4 : 0x04b2, - 0x30A6 : 0x04b3, - 0x30A8 : 0x04b4, - 0x30AA : 0x04b5, - 0x30AB : 0x04b6, - 0x30AD : 0x04b7, - 0x30AF : 0x04b8, - 0x30B1 : 0x04b9, - 0x30B3 : 0x04ba, - 0x30B5 : 0x04bb, - 0x30B7 : 0x04bc, - 0x30B9 : 0x04bd, - 0x30BB : 0x04be, - 0x30BD : 0x04bf, - 0x30BF : 0x04c0, - 0x30C1 : 0x04c1, - 0x30C4 : 0x04c2, - 0x30C6 : 0x04c3, - 0x30C8 : 0x04c4, - 0x30CA : 0x04c5, - 0x30CB : 0x04c6, - 0x30CC : 0x04c7, - 0x30CD : 0x04c8, - 0x30CE : 0x04c9, - 0x30CF : 0x04ca, - 0x30D2 : 0x04cb, - 0x30D5 : 0x04cc, - 0x30D8 : 0x04cd, - 0x30DB : 0x04ce, - 0x30DE : 0x04cf, - 0x30DF : 0x04d0, - 0x30E0 : 0x04d1, - 0x30E1 : 0x04d2, - 0x30E2 : 0x04d3, - 0x30E4 : 0x04d4, - 0x30E6 : 0x04d5, - 0x30E8 : 0x04d6, - 0x30E9 : 0x04d7, - 0x30EA : 0x04d8, - 0x30EB : 0x04d9, - 0x30EC : 0x04da, - 0x30ED : 0x04db, - 0x30EF : 0x04dc, - 0x30F3 : 0x04dd, - 0x309B : 0x04de, - 0x309C : 0x04df, - 0x06F0 : 0x10006f0, - 0x06F1 : 0x10006f1, - 0x06F2 : 0x10006f2, - 0x06F3 : 0x10006f3, - 0x06F4 : 0x10006f4, - 0x06F5 : 0x10006f5, - 0x06F6 : 0x10006f6, - 0x06F7 : 0x10006f7, - 0x06F8 : 0x10006f8, - 0x06F9 : 0x10006f9, - 0x066A : 0x100066a, - 0x0670 : 0x1000670, - 0x0679 : 0x1000679, - 0x067E : 0x100067e, - 0x0686 : 0x1000686, - 0x0688 : 0x1000688, - 0x0691 : 0x1000691, - 0x060C : 0x05ac, - 0x06D4 : 0x10006d4, - 0x0660 : 0x1000660, - 0x0661 : 0x1000661, - 0x0662 : 0x1000662, - 0x0663 : 0x1000663, - 0x0664 : 0x1000664, - 0x0665 : 0x1000665, - 0x0666 : 0x1000666, - 0x0667 : 0x1000667, - 0x0668 : 0x1000668, - 0x0669 : 0x1000669, - 0x061B : 0x05bb, - 0x061F : 0x05bf, - 0x0621 : 0x05c1, - 0x0622 : 0x05c2, - 0x0623 : 0x05c3, - 0x0624 : 0x05c4, - 0x0625 : 0x05c5, - 0x0626 : 0x05c6, - 0x0627 : 0x05c7, - 0x0628 : 0x05c8, - 0x0629 : 0x05c9, - 0x062A : 0x05ca, - 0x062B : 0x05cb, - 0x062C : 0x05cc, - 0x062D : 0x05cd, - 0x062E : 0x05ce, - 0x062F : 0x05cf, - 0x0630 : 0x05d0, - 0x0631 : 0x05d1, - 0x0632 : 0x05d2, - 0x0633 : 0x05d3, - 0x0634 : 0x05d4, - 0x0635 : 0x05d5, - 0x0636 : 0x05d6, - 0x0637 : 0x05d7, - 0x0638 : 0x05d8, - 0x0639 : 0x05d9, - 0x063A : 0x05da, - 0x0640 : 0x05e0, - 0x0641 : 0x05e1, - 0x0642 : 0x05e2, - 0x0643 : 0x05e3, - 0x0644 : 0x05e4, - 0x0645 : 0x05e5, - 0x0646 : 0x05e6, - 0x0647 : 0x05e7, - 0x0648 : 0x05e8, - 0x0649 : 0x05e9, - 0x064A : 0x05ea, - 0x064B : 0x05eb, - 0x064C : 0x05ec, - 0x064D : 0x05ed, - 0x064E : 0x05ee, - 0x064F : 0x05ef, - 0x0650 : 0x05f0, - 0x0651 : 0x05f1, - 0x0652 : 0x05f2, - 0x0653 : 0x1000653, - 0x0654 : 0x1000654, - 0x0655 : 0x1000655, - 0x0698 : 0x1000698, - 0x06A4 : 0x10006a4, - 0x06A9 : 0x10006a9, - 0x06AF : 0x10006af, - 0x06BA : 0x10006ba, - 0x06BE : 0x10006be, - 0x06CC : 0x10006cc, - 0x06D2 : 0x10006d2, - 0x06C1 : 0x10006c1, - 0x0492 : 0x1000492, - 0x0493 : 0x1000493, - 0x0496 : 0x1000496, - 0x0497 : 0x1000497, - 0x049A : 0x100049a, - 0x049B : 0x100049b, - 0x049C : 0x100049c, - 0x049D : 0x100049d, - 0x04A2 : 0x10004a2, - 0x04A3 : 0x10004a3, - 0x04AE : 0x10004ae, - 0x04AF : 0x10004af, - 0x04B0 : 0x10004b0, - 0x04B1 : 0x10004b1, - 0x04B2 : 0x10004b2, - 0x04B3 : 0x10004b3, - 0x04B6 : 0x10004b6, - 0x04B7 : 0x10004b7, - 0x04B8 : 0x10004b8, - 0x04B9 : 0x10004b9, - 0x04BA : 0x10004ba, - 0x04BB : 0x10004bb, - 0x04D8 : 0x10004d8, - 0x04D9 : 0x10004d9, - 0x04E2 : 0x10004e2, - 0x04E3 : 0x10004e3, - 0x04E8 : 0x10004e8, - 0x04E9 : 0x10004e9, - 0x04EE : 0x10004ee, - 0x04EF : 0x10004ef, - 0x0452 : 0x06a1, - 0x0453 : 0x06a2, - 0x0451 : 0x06a3, - 0x0454 : 0x06a4, - 0x0455 : 0x06a5, - 0x0456 : 0x06a6, - 0x0457 : 0x06a7, - 0x0458 : 0x06a8, - 0x0459 : 0x06a9, - 0x045A : 0x06aa, - 0x045B : 0x06ab, - 0x045C : 0x06ac, - 0x0491 : 0x06ad, - 0x045E : 0x06ae, - 0x045F : 0x06af, - 0x2116 : 0x06b0, - 0x0402 : 0x06b1, - 0x0403 : 0x06b2, - 0x0401 : 0x06b3, - 0x0404 : 0x06b4, - 0x0405 : 0x06b5, - 0x0406 : 0x06b6, - 0x0407 : 0x06b7, - 0x0408 : 0x06b8, - 0x0409 : 0x06b9, - 0x040A : 0x06ba, - 0x040B : 0x06bb, - 0x040C : 0x06bc, - 0x0490 : 0x06bd, - 0x040E : 0x06be, - 0x040F : 0x06bf, - 0x044E : 0x06c0, - 0x0430 : 0x06c1, - 0x0431 : 0x06c2, - 0x0446 : 0x06c3, - 0x0434 : 0x06c4, - 0x0435 : 0x06c5, - 0x0444 : 0x06c6, - 0x0433 : 0x06c7, - 0x0445 : 0x06c8, - 0x0438 : 0x06c9, - 0x0439 : 0x06ca, - 0x043A : 0x06cb, - 0x043B : 0x06cc, - 0x043C : 0x06cd, - 0x043D : 0x06ce, - 0x043E : 0x06cf, - 0x043F : 0x06d0, - 0x044F : 0x06d1, - 0x0440 : 0x06d2, - 0x0441 : 0x06d3, - 0x0442 : 0x06d4, - 0x0443 : 0x06d5, - 0x0436 : 0x06d6, - 0x0432 : 0x06d7, - 0x044C : 0x06d8, - 0x044B : 0x06d9, - 0x0437 : 0x06da, - 0x0448 : 0x06db, - 0x044D : 0x06dc, - 0x0449 : 0x06dd, - 0x0447 : 0x06de, - 0x044A : 0x06df, - 0x042E : 0x06e0, - 0x0410 : 0x06e1, - 0x0411 : 0x06e2, - 0x0426 : 0x06e3, - 0x0414 : 0x06e4, - 0x0415 : 0x06e5, - 0x0424 : 0x06e6, - 0x0413 : 0x06e7, - 0x0425 : 0x06e8, - 0x0418 : 0x06e9, - 0x0419 : 0x06ea, - 0x041A : 0x06eb, - 0x041B : 0x06ec, - 0x041C : 0x06ed, - 0x041D : 0x06ee, - 0x041E : 0x06ef, - 0x041F : 0x06f0, - 0x042F : 0x06f1, - 0x0420 : 0x06f2, - 0x0421 : 0x06f3, - 0x0422 : 0x06f4, - 0x0423 : 0x06f5, - 0x0416 : 0x06f6, - 0x0412 : 0x06f7, - 0x042C : 0x06f8, - 0x042B : 0x06f9, - 0x0417 : 0x06fa, - 0x0428 : 0x06fb, - 0x042D : 0x06fc, - 0x0429 : 0x06fd, - 0x0427 : 0x06fe, - 0x042A : 0x06ff, - 0x0386 : 0x07a1, - 0x0388 : 0x07a2, - 0x0389 : 0x07a3, - 0x038A : 0x07a4, - 0x03AA : 0x07a5, - 0x038C : 0x07a7, - 0x038E : 0x07a8, - 0x03AB : 0x07a9, - 0x038F : 0x07ab, - 0x0385 : 0x07ae, - 0x2015 : 0x07af, - 0x03AC : 0x07b1, - 0x03AD : 0x07b2, - 0x03AE : 0x07b3, - 0x03AF : 0x07b4, - 0x03CA : 0x07b5, - 0x0390 : 0x07b6, - 0x03CC : 0x07b7, - 0x03CD : 0x07b8, - 0x03CB : 0x07b9, - 0x03B0 : 0x07ba, - 0x03CE : 0x07bb, - 0x0391 : 0x07c1, - 0x0392 : 0x07c2, - 0x0393 : 0x07c3, - 0x0394 : 0x07c4, - 0x0395 : 0x07c5, - 0x0396 : 0x07c6, - 0x0397 : 0x07c7, - 0x0398 : 0x07c8, - 0x0399 : 0x07c9, - 0x039A : 0x07ca, - 0x039B : 0x07cb, - 0x039C : 0x07cc, - 0x039D : 0x07cd, - 0x039E : 0x07ce, - 0x039F : 0x07cf, - 0x03A0 : 0x07d0, - 0x03A1 : 0x07d1, - 0x03A3 : 0x07d2, - 0x03A4 : 0x07d4, - 0x03A5 : 0x07d5, - 0x03A6 : 0x07d6, - 0x03A7 : 0x07d7, - 0x03A8 : 0x07d8, - 0x03A9 : 0x07d9, - 0x03B1 : 0x07e1, - 0x03B2 : 0x07e2, - 0x03B3 : 0x07e3, - 0x03B4 : 0x07e4, - 0x03B5 : 0x07e5, - 0x03B6 : 0x07e6, - 0x03B7 : 0x07e7, - 0x03B8 : 0x07e8, - 0x03B9 : 0x07e9, - 0x03BA : 0x07ea, - 0x03BB : 0x07eb, - 0x03BC : 0x07ec, - 0x03BD : 0x07ed, - 0x03BE : 0x07ee, - 0x03BF : 0x07ef, - 0x03C0 : 0x07f0, - 0x03C1 : 0x07f1, - 0x03C3 : 0x07f2, - 0x03C2 : 0x07f3, - 0x03C4 : 0x07f4, - 0x03C5 : 0x07f5, - 0x03C6 : 0x07f6, - 0x03C7 : 0x07f7, - 0x03C8 : 0x07f8, - 0x03C9 : 0x07f9, - 0x23B7 : 0x08a1, - 0x2320 : 0x08a4, - 0x2321 : 0x08a5, - 0x23A1 : 0x08a7, - 0x23A3 : 0x08a8, - 0x23A4 : 0x08a9, - 0x23A6 : 0x08aa, - 0x239B : 0x08ab, - 0x239D : 0x08ac, - 0x239E : 0x08ad, - 0x23A0 : 0x08ae, - 0x23A8 : 0x08af, - 0x23AC : 0x08b0, - 0x2264 : 0x08bc, - 0x2260 : 0x08bd, - 0x2265 : 0x08be, - 0x222B : 0x08bf, - 0x2234 : 0x08c0, - 0x221D : 0x08c1, - 0x221E : 0x08c2, - 0x2207 : 0x08c5, - 0x223C : 0x08c8, - 0x2243 : 0x08c9, - 0x21D4 : 0x08cd, - 0x21D2 : 0x08ce, - 0x2261 : 0x08cf, - //0x221A : 0x08d6, - 0x2282 : 0x08da, - 0x2283 : 0x08db, - 0x2229 : 0x08dc, - 0x222A : 0x08dd, - 0x2227 : 0x08de, - 0x2228 : 0x08df, - //0x2202 : 0x08ef, - 0x0192 : 0x08f6, - 0x2190 : 0x08fb, - 0x2191 : 0x08fc, - 0x2192 : 0x08fd, - 0x2193 : 0x08fe, - 0x25C6 : 0x09e0, - 0x2592 : 0x09e1, - 0x2409 : 0x09e2, - 0x240C : 0x09e3, - 0x240D : 0x09e4, - 0x240A : 0x09e5, - 0x2424 : 0x09e8, - 0x240B : 0x09e9, - 0x2518 : 0x09ea, - 0x2510 : 0x09eb, - 0x250C : 0x09ec, - 0x2514 : 0x09ed, - 0x253C : 0x09ee, - 0x23BA : 0x09ef, - 0x23BB : 0x09f0, - 0x2500 : 0x09f1, - 0x23BC : 0x09f2, - 0x23BD : 0x09f3, - 0x251C : 0x09f4, - 0x2524 : 0x09f5, - 0x2534 : 0x09f6, - 0x252C : 0x09f7, - 0x2502 : 0x09f8, - 0x2003 : 0x0aa1, - 0x2002 : 0x0aa2, - 0x2004 : 0x0aa3, - 0x2005 : 0x0aa4, - 0x2007 : 0x0aa5, - 0x2008 : 0x0aa6, - 0x2009 : 0x0aa7, - 0x200A : 0x0aa8, - 0x2014 : 0x0aa9, - 0x2013 : 0x0aaa, - 0x2026 : 0x0aae, - 0x2025 : 0x0aaf, - 0x2153 : 0x0ab0, - 0x2154 : 0x0ab1, - 0x2155 : 0x0ab2, - 0x2156 : 0x0ab3, - 0x2157 : 0x0ab4, - 0x2158 : 0x0ab5, - 0x2159 : 0x0ab6, - 0x215A : 0x0ab7, - 0x2105 : 0x0ab8, - 0x2012 : 0x0abb, - 0x215B : 0x0ac3, - 0x215C : 0x0ac4, - 0x215D : 0x0ac5, - 0x215E : 0x0ac6, - 0x2122 : 0x0ac9, - 0x2018 : 0x0ad0, - 0x2019 : 0x0ad1, - 0x201C : 0x0ad2, - 0x201D : 0x0ad3, - 0x211E : 0x0ad4, - 0x2032 : 0x0ad6, - 0x2033 : 0x0ad7, - 0x271D : 0x0ad9, - 0x2663 : 0x0aec, - 0x2666 : 0x0aed, - 0x2665 : 0x0aee, - 0x2720 : 0x0af0, - 0x2020 : 0x0af1, - 0x2021 : 0x0af2, - 0x2713 : 0x0af3, - 0x2717 : 0x0af4, - 0x266F : 0x0af5, - 0x266D : 0x0af6, - 0x2642 : 0x0af7, - 0x2640 : 0x0af8, - 0x260E : 0x0af9, - 0x2315 : 0x0afa, - 0x2117 : 0x0afb, - 0x2038 : 0x0afc, - 0x201A : 0x0afd, - 0x201E : 0x0afe, - 0x22A4 : 0x0bc2, - 0x230A : 0x0bc4, - 0x2218 : 0x0bca, - 0x2395 : 0x0bcc, - 0x22A5 : 0x0bce, - 0x25CB : 0x0bcf, - 0x2308 : 0x0bd3, - 0x22A3 : 0x0bdc, - 0x22A2 : 0x0bfc, - 0x2017 : 0x0cdf, - 0x05D0 : 0x0ce0, - 0x05D1 : 0x0ce1, - 0x05D2 : 0x0ce2, - 0x05D3 : 0x0ce3, - 0x05D4 : 0x0ce4, - 0x05D5 : 0x0ce5, - 0x05D6 : 0x0ce6, - 0x05D7 : 0x0ce7, - 0x05D8 : 0x0ce8, - 0x05D9 : 0x0ce9, - 0x05DA : 0x0cea, - 0x05DB : 0x0ceb, - 0x05DC : 0x0cec, - 0x05DD : 0x0ced, - 0x05DE : 0x0cee, - 0x05DF : 0x0cef, - 0x05E0 : 0x0cf0, - 0x05E1 : 0x0cf1, - 0x05E2 : 0x0cf2, - 0x05E3 : 0x0cf3, - 0x05E4 : 0x0cf4, - 0x05E5 : 0x0cf5, - 0x05E6 : 0x0cf6, - 0x05E7 : 0x0cf7, - 0x05E8 : 0x0cf8, - 0x05E9 : 0x0cf9, - 0x05EA : 0x0cfa, - 0x0E01 : 0x0da1, - 0x0E02 : 0x0da2, - 0x0E03 : 0x0da3, - 0x0E04 : 0x0da4, - 0x0E05 : 0x0da5, - 0x0E06 : 0x0da6, - 0x0E07 : 0x0da7, - 0x0E08 : 0x0da8, - 0x0E09 : 0x0da9, - 0x0E0A : 0x0daa, - 0x0E0B : 0x0dab, - 0x0E0C : 0x0dac, - 0x0E0D : 0x0dad, - 0x0E0E : 0x0dae, - 0x0E0F : 0x0daf, - 0x0E10 : 0x0db0, - 0x0E11 : 0x0db1, - 0x0E12 : 0x0db2, - 0x0E13 : 0x0db3, - 0x0E14 : 0x0db4, - 0x0E15 : 0x0db5, - 0x0E16 : 0x0db6, - 0x0E17 : 0x0db7, - 0x0E18 : 0x0db8, - 0x0E19 : 0x0db9, - 0x0E1A : 0x0dba, - 0x0E1B : 0x0dbb, - 0x0E1C : 0x0dbc, - 0x0E1D : 0x0dbd, - 0x0E1E : 0x0dbe, - 0x0E1F : 0x0dbf, - 0x0E20 : 0x0dc0, - 0x0E21 : 0x0dc1, - 0x0E22 : 0x0dc2, - 0x0E23 : 0x0dc3, - 0x0E24 : 0x0dc4, - 0x0E25 : 0x0dc5, - 0x0E26 : 0x0dc6, - 0x0E27 : 0x0dc7, - 0x0E28 : 0x0dc8, - 0x0E29 : 0x0dc9, - 0x0E2A : 0x0dca, - 0x0E2B : 0x0dcb, - 0x0E2C : 0x0dcc, - 0x0E2D : 0x0dcd, - 0x0E2E : 0x0dce, - 0x0E2F : 0x0dcf, - 0x0E30 : 0x0dd0, - 0x0E31 : 0x0dd1, - 0x0E32 : 0x0dd2, - 0x0E33 : 0x0dd3, - 0x0E34 : 0x0dd4, - 0x0E35 : 0x0dd5, - 0x0E36 : 0x0dd6, - 0x0E37 : 0x0dd7, - 0x0E38 : 0x0dd8, - 0x0E39 : 0x0dd9, - 0x0E3A : 0x0dda, - 0x0E3F : 0x0ddf, - 0x0E40 : 0x0de0, - 0x0E41 : 0x0de1, - 0x0E42 : 0x0de2, - 0x0E43 : 0x0de3, - 0x0E44 : 0x0de4, - 0x0E45 : 0x0de5, - 0x0E46 : 0x0de6, - 0x0E47 : 0x0de7, - 0x0E48 : 0x0de8, - 0x0E49 : 0x0de9, - 0x0E4A : 0x0dea, - 0x0E4B : 0x0deb, - 0x0E4C : 0x0dec, - 0x0E4D : 0x0ded, - 0x0E50 : 0x0df0, - 0x0E51 : 0x0df1, - 0x0E52 : 0x0df2, - 0x0E53 : 0x0df3, - 0x0E54 : 0x0df4, - 0x0E55 : 0x0df5, - 0x0E56 : 0x0df6, - 0x0E57 : 0x0df7, - 0x0E58 : 0x0df8, - 0x0E59 : 0x0df9, - 0x0587 : 0x1000587, - 0x0589 : 0x1000589, - 0x055D : 0x100055d, - 0x058A : 0x100058a, - 0x055C : 0x100055c, - 0x055B : 0x100055b, - 0x055E : 0x100055e, - 0x0531 : 0x1000531, - 0x0561 : 0x1000561, - 0x0532 : 0x1000532, - 0x0562 : 0x1000562, - 0x0533 : 0x1000533, - 0x0563 : 0x1000563, - 0x0534 : 0x1000534, - 0x0564 : 0x1000564, - 0x0535 : 0x1000535, - 0x0565 : 0x1000565, - 0x0536 : 0x1000536, - 0x0566 : 0x1000566, - 0x0537 : 0x1000537, - 0x0567 : 0x1000567, - 0x0538 : 0x1000538, - 0x0568 : 0x1000568, - 0x0539 : 0x1000539, - 0x0569 : 0x1000569, - 0x053A : 0x100053a, - 0x056A : 0x100056a, - 0x053B : 0x100053b, - 0x056B : 0x100056b, - 0x053C : 0x100053c, - 0x056C : 0x100056c, - 0x053D : 0x100053d, - 0x056D : 0x100056d, - 0x053E : 0x100053e, - 0x056E : 0x100056e, - 0x053F : 0x100053f, - 0x056F : 0x100056f, - 0x0540 : 0x1000540, - 0x0570 : 0x1000570, - 0x0541 : 0x1000541, - 0x0571 : 0x1000571, - 0x0542 : 0x1000542, - 0x0572 : 0x1000572, - 0x0543 : 0x1000543, - 0x0573 : 0x1000573, - 0x0544 : 0x1000544, - 0x0574 : 0x1000574, - 0x0545 : 0x1000545, - 0x0575 : 0x1000575, - 0x0546 : 0x1000546, - 0x0576 : 0x1000576, - 0x0547 : 0x1000547, - 0x0577 : 0x1000577, - 0x0548 : 0x1000548, - 0x0578 : 0x1000578, - 0x0549 : 0x1000549, - 0x0579 : 0x1000579, - 0x054A : 0x100054a, - 0x057A : 0x100057a, - 0x054B : 0x100054b, - 0x057B : 0x100057b, - 0x054C : 0x100054c, - 0x057C : 0x100057c, - 0x054D : 0x100054d, - 0x057D : 0x100057d, - 0x054E : 0x100054e, - 0x057E : 0x100057e, - 0x054F : 0x100054f, - 0x057F : 0x100057f, - 0x0550 : 0x1000550, - 0x0580 : 0x1000580, - 0x0551 : 0x1000551, - 0x0581 : 0x1000581, - 0x0552 : 0x1000552, - 0x0582 : 0x1000582, - 0x0553 : 0x1000553, - 0x0583 : 0x1000583, - 0x0554 : 0x1000554, - 0x0584 : 0x1000584, - 0x0555 : 0x1000555, - 0x0585 : 0x1000585, - 0x0556 : 0x1000556, - 0x0586 : 0x1000586, - 0x055A : 0x100055a, - 0x10D0 : 0x10010d0, - 0x10D1 : 0x10010d1, - 0x10D2 : 0x10010d2, - 0x10D3 : 0x10010d3, - 0x10D4 : 0x10010d4, - 0x10D5 : 0x10010d5, - 0x10D6 : 0x10010d6, - 0x10D7 : 0x10010d7, - 0x10D8 : 0x10010d8, - 0x10D9 : 0x10010d9, - 0x10DA : 0x10010da, - 0x10DB : 0x10010db, - 0x10DC : 0x10010dc, - 0x10DD : 0x10010dd, - 0x10DE : 0x10010de, - 0x10DF : 0x10010df, - 0x10E0 : 0x10010e0, - 0x10E1 : 0x10010e1, - 0x10E2 : 0x10010e2, - 0x10E3 : 0x10010e3, - 0x10E4 : 0x10010e4, - 0x10E5 : 0x10010e5, - 0x10E6 : 0x10010e6, - 0x10E7 : 0x10010e7, - 0x10E8 : 0x10010e8, - 0x10E9 : 0x10010e9, - 0x10EA : 0x10010ea, - 0x10EB : 0x10010eb, - 0x10EC : 0x10010ec, - 0x10ED : 0x10010ed, - 0x10EE : 0x10010ee, - 0x10EF : 0x10010ef, - 0x10F0 : 0x10010f0, - 0x10F1 : 0x10010f1, - 0x10F2 : 0x10010f2, - 0x10F3 : 0x10010f3, - 0x10F4 : 0x10010f4, - 0x10F5 : 0x10010f5, - 0x10F6 : 0x10010f6, - 0x1E8A : 0x1001e8a, - 0x012C : 0x100012c, - 0x01B5 : 0x10001b5, - 0x01E6 : 0x10001e6, - 0x01D2 : 0x10001d1, - 0x019F : 0x100019f, - 0x1E8B : 0x1001e8b, - 0x012D : 0x100012d, - 0x01B6 : 0x10001b6, - 0x01E7 : 0x10001e7, - //0x01D2 : 0x10001d2, - 0x0275 : 0x1000275, - 0x018F : 0x100018f, - 0x0259 : 0x1000259, - 0x1E36 : 0x1001e36, - 0x1E37 : 0x1001e37, - 0x1EA0 : 0x1001ea0, - 0x1EA1 : 0x1001ea1, - 0x1EA2 : 0x1001ea2, - 0x1EA3 : 0x1001ea3, - 0x1EA4 : 0x1001ea4, - 0x1EA5 : 0x1001ea5, - 0x1EA6 : 0x1001ea6, - 0x1EA7 : 0x1001ea7, - 0x1EA8 : 0x1001ea8, - 0x1EA9 : 0x1001ea9, - 0x1EAA : 0x1001eaa, - 0x1EAB : 0x1001eab, - 0x1EAC : 0x1001eac, - 0x1EAD : 0x1001ead, - 0x1EAE : 0x1001eae, - 0x1EAF : 0x1001eaf, - 0x1EB0 : 0x1001eb0, - 0x1EB1 : 0x1001eb1, - 0x1EB2 : 0x1001eb2, - 0x1EB3 : 0x1001eb3, - 0x1EB4 : 0x1001eb4, - 0x1EB5 : 0x1001eb5, - 0x1EB6 : 0x1001eb6, - 0x1EB7 : 0x1001eb7, - 0x1EB8 : 0x1001eb8, - 0x1EB9 : 0x1001eb9, - 0x1EBA : 0x1001eba, - 0x1EBB : 0x1001ebb, - 0x1EBC : 0x1001ebc, - 0x1EBD : 0x1001ebd, - 0x1EBE : 0x1001ebe, - 0x1EBF : 0x1001ebf, - 0x1EC0 : 0x1001ec0, - 0x1EC1 : 0x1001ec1, - 0x1EC2 : 0x1001ec2, - 0x1EC3 : 0x1001ec3, - 0x1EC4 : 0x1001ec4, - 0x1EC5 : 0x1001ec5, - 0x1EC6 : 0x1001ec6, - 0x1EC7 : 0x1001ec7, - 0x1EC8 : 0x1001ec8, - 0x1EC9 : 0x1001ec9, - 0x1ECA : 0x1001eca, - 0x1ECB : 0x1001ecb, - 0x1ECC : 0x1001ecc, - 0x1ECD : 0x1001ecd, - 0x1ECE : 0x1001ece, - 0x1ECF : 0x1001ecf, - 0x1ED0 : 0x1001ed0, - 0x1ED1 : 0x1001ed1, - 0x1ED2 : 0x1001ed2, - 0x1ED3 : 0x1001ed3, - 0x1ED4 : 0x1001ed4, - 0x1ED5 : 0x1001ed5, - 0x1ED6 : 0x1001ed6, - 0x1ED7 : 0x1001ed7, - 0x1ED8 : 0x1001ed8, - 0x1ED9 : 0x1001ed9, - 0x1EDA : 0x1001eda, - 0x1EDB : 0x1001edb, - 0x1EDC : 0x1001edc, - 0x1EDD : 0x1001edd, - 0x1EDE : 0x1001ede, - 0x1EDF : 0x1001edf, - 0x1EE0 : 0x1001ee0, - 0x1EE1 : 0x1001ee1, - 0x1EE2 : 0x1001ee2, - 0x1EE3 : 0x1001ee3, - 0x1EE4 : 0x1001ee4, - 0x1EE5 : 0x1001ee5, - 0x1EE6 : 0x1001ee6, - 0x1EE7 : 0x1001ee7, - 0x1EE8 : 0x1001ee8, - 0x1EE9 : 0x1001ee9, - 0x1EEA : 0x1001eea, - 0x1EEB : 0x1001eeb, - 0x1EEC : 0x1001eec, - 0x1EED : 0x1001eed, - 0x1EEE : 0x1001eee, - 0x1EEF : 0x1001eef, - 0x1EF0 : 0x1001ef0, - 0x1EF1 : 0x1001ef1, - 0x1EF4 : 0x1001ef4, - 0x1EF5 : 0x1001ef5, - 0x1EF6 : 0x1001ef6, - 0x1EF7 : 0x1001ef7, - 0x1EF8 : 0x1001ef8, - 0x1EF9 : 0x1001ef9, - 0x01A0 : 0x10001a0, - 0x01A1 : 0x10001a1, - 0x01AF : 0x10001af, - 0x01B0 : 0x10001b0, - 0x20A0 : 0x10020a0, - 0x20A1 : 0x10020a1, - 0x20A2 : 0x10020a2, - 0x20A3 : 0x10020a3, - 0x20A4 : 0x10020a4, - 0x20A5 : 0x10020a5, - 0x20A6 : 0x10020a6, - 0x20A7 : 0x10020a7, - 0x20A8 : 0x10020a8, - 0x20A9 : 0x10020a9, - 0x20AA : 0x10020aa, - 0x20AB : 0x10020ab, - 0x20AC : 0x20ac, - 0x2070 : 0x1002070, - 0x2074 : 0x1002074, - 0x2075 : 0x1002075, - 0x2076 : 0x1002076, - 0x2077 : 0x1002077, - 0x2078 : 0x1002078, - 0x2079 : 0x1002079, - 0x2080 : 0x1002080, - 0x2081 : 0x1002081, - 0x2082 : 0x1002082, - 0x2083 : 0x1002083, - 0x2084 : 0x1002084, - 0x2085 : 0x1002085, - 0x2086 : 0x1002086, - 0x2087 : 0x1002087, - 0x2088 : 0x1002088, - 0x2089 : 0x1002089, - 0x2202 : 0x1002202, - 0x2205 : 0x1002205, - 0x2208 : 0x1002208, - 0x2209 : 0x1002209, - 0x220B : 0x100220B, - 0x221A : 0x100221A, - 0x221B : 0x100221B, - 0x221C : 0x100221C, - 0x222C : 0x100222C, - 0x222D : 0x100222D, - 0x2235 : 0x1002235, - 0x2245 : 0x1002248, - 0x2247 : 0x1002247, - 0x2262 : 0x1002262, - 0x2263 : 0x1002263, - 0x2800 : 0x1002800, - 0x2801 : 0x1002801, - 0x2802 : 0x1002802, - 0x2803 : 0x1002803, - 0x2804 : 0x1002804, - 0x2805 : 0x1002805, - 0x2806 : 0x1002806, - 0x2807 : 0x1002807, - 0x2808 : 0x1002808, - 0x2809 : 0x1002809, - 0x280a : 0x100280a, - 0x280b : 0x100280b, - 0x280c : 0x100280c, - 0x280d : 0x100280d, - 0x280e : 0x100280e, - 0x280f : 0x100280f, - 0x2810 : 0x1002810, - 0x2811 : 0x1002811, - 0x2812 : 0x1002812, - 0x2813 : 0x1002813, - 0x2814 : 0x1002814, - 0x2815 : 0x1002815, - 0x2816 : 0x1002816, - 0x2817 : 0x1002817, - 0x2818 : 0x1002818, - 0x2819 : 0x1002819, - 0x281a : 0x100281a, - 0x281b : 0x100281b, - 0x281c : 0x100281c, - 0x281d : 0x100281d, - 0x281e : 0x100281e, - 0x281f : 0x100281f, - 0x2820 : 0x1002820, - 0x2821 : 0x1002821, - 0x2822 : 0x1002822, - 0x2823 : 0x1002823, - 0x2824 : 0x1002824, - 0x2825 : 0x1002825, - 0x2826 : 0x1002826, - 0x2827 : 0x1002827, - 0x2828 : 0x1002828, - 0x2829 : 0x1002829, - 0x282a : 0x100282a, - 0x282b : 0x100282b, - 0x282c : 0x100282c, - 0x282d : 0x100282d, - 0x282e : 0x100282e, - 0x282f : 0x100282f, - 0x2830 : 0x1002830, - 0x2831 : 0x1002831, - 0x2832 : 0x1002832, - 0x2833 : 0x1002833, - 0x2834 : 0x1002834, - 0x2835 : 0x1002835, - 0x2836 : 0x1002836, - 0x2837 : 0x1002837, - 0x2838 : 0x1002838, - 0x2839 : 0x1002839, - 0x283a : 0x100283a, - 0x283b : 0x100283b, - 0x283c : 0x100283c, - 0x283d : 0x100283d, - 0x283e : 0x100283e, - 0x283f : 0x100283f, - 0x2840 : 0x1002840, - 0x2841 : 0x1002841, - 0x2842 : 0x1002842, - 0x2843 : 0x1002843, - 0x2844 : 0x1002844, - 0x2845 : 0x1002845, - 0x2846 : 0x1002846, - 0x2847 : 0x1002847, - 0x2848 : 0x1002848, - 0x2849 : 0x1002849, - 0x284a : 0x100284a, - 0x284b : 0x100284b, - 0x284c : 0x100284c, - 0x284d : 0x100284d, - 0x284e : 0x100284e, - 0x284f : 0x100284f, - 0x2850 : 0x1002850, - 0x2851 : 0x1002851, - 0x2852 : 0x1002852, - 0x2853 : 0x1002853, - 0x2854 : 0x1002854, - 0x2855 : 0x1002855, - 0x2856 : 0x1002856, - 0x2857 : 0x1002857, - 0x2858 : 0x1002858, - 0x2859 : 0x1002859, - 0x285a : 0x100285a, - 0x285b : 0x100285b, - 0x285c : 0x100285c, - 0x285d : 0x100285d, - 0x285e : 0x100285e, - 0x285f : 0x100285f, - 0x2860 : 0x1002860, - 0x2861 : 0x1002861, - 0x2862 : 0x1002862, - 0x2863 : 0x1002863, - 0x2864 : 0x1002864, - 0x2865 : 0x1002865, - 0x2866 : 0x1002866, - 0x2867 : 0x1002867, - 0x2868 : 0x1002868, - 0x2869 : 0x1002869, - 0x286a : 0x100286a, - 0x286b : 0x100286b, - 0x286c : 0x100286c, - 0x286d : 0x100286d, - 0x286e : 0x100286e, - 0x286f : 0x100286f, - 0x2870 : 0x1002870, - 0x2871 : 0x1002871, - 0x2872 : 0x1002872, - 0x2873 : 0x1002873, - 0x2874 : 0x1002874, - 0x2875 : 0x1002875, - 0x2876 : 0x1002876, - 0x2877 : 0x1002877, - 0x2878 : 0x1002878, - 0x2879 : 0x1002879, - 0x287a : 0x100287a, - 0x287b : 0x100287b, - 0x287c : 0x100287c, - 0x287d : 0x100287d, - 0x287e : 0x100287e, - 0x287f : 0x100287f, - 0x2880 : 0x1002880, - 0x2881 : 0x1002881, - 0x2882 : 0x1002882, - 0x2883 : 0x1002883, - 0x2884 : 0x1002884, - 0x2885 : 0x1002885, - 0x2886 : 0x1002886, - 0x2887 : 0x1002887, - 0x2888 : 0x1002888, - 0x2889 : 0x1002889, - 0x288a : 0x100288a, - 0x288b : 0x100288b, - 0x288c : 0x100288c, - 0x288d : 0x100288d, - 0x288e : 0x100288e, - 0x288f : 0x100288f, - 0x2890 : 0x1002890, - 0x2891 : 0x1002891, - 0x2892 : 0x1002892, - 0x2893 : 0x1002893, - 0x2894 : 0x1002894, - 0x2895 : 0x1002895, - 0x2896 : 0x1002896, - 0x2897 : 0x1002897, - 0x2898 : 0x1002898, - 0x2899 : 0x1002899, - 0x289a : 0x100289a, - 0x289b : 0x100289b, - 0x289c : 0x100289c, - 0x289d : 0x100289d, - 0x289e : 0x100289e, - 0x289f : 0x100289f, - 0x28a0 : 0x10028a0, - 0x28a1 : 0x10028a1, - 0x28a2 : 0x10028a2, - 0x28a3 : 0x10028a3, - 0x28a4 : 0x10028a4, - 0x28a5 : 0x10028a5, - 0x28a6 : 0x10028a6, - 0x28a7 : 0x10028a7, - 0x28a8 : 0x10028a8, - 0x28a9 : 0x10028a9, - 0x28aa : 0x10028aa, - 0x28ab : 0x10028ab, - 0x28ac : 0x10028ac, - 0x28ad : 0x10028ad, - 0x28ae : 0x10028ae, - 0x28af : 0x10028af, - 0x28b0 : 0x10028b0, - 0x28b1 : 0x10028b1, - 0x28b2 : 0x10028b2, - 0x28b3 : 0x10028b3, - 0x28b4 : 0x10028b4, - 0x28b5 : 0x10028b5, - 0x28b6 : 0x10028b6, - 0x28b7 : 0x10028b7, - 0x28b8 : 0x10028b8, - 0x28b9 : 0x10028b9, - 0x28ba : 0x10028ba, - 0x28bb : 0x10028bb, - 0x28bc : 0x10028bc, - 0x28bd : 0x10028bd, - 0x28be : 0x10028be, - 0x28bf : 0x10028bf, - 0x28c0 : 0x10028c0, - 0x28c1 : 0x10028c1, - 0x28c2 : 0x10028c2, - 0x28c3 : 0x10028c3, - 0x28c4 : 0x10028c4, - 0x28c5 : 0x10028c5, - 0x28c6 : 0x10028c6, - 0x28c7 : 0x10028c7, - 0x28c8 : 0x10028c8, - 0x28c9 : 0x10028c9, - 0x28ca : 0x10028ca, - 0x28cb : 0x10028cb, - 0x28cc : 0x10028cc, - 0x28cd : 0x10028cd, - 0x28ce : 0x10028ce, - 0x28cf : 0x10028cf, - 0x28d0 : 0x10028d0, - 0x28d1 : 0x10028d1, - 0x28d2 : 0x10028d2, - 0x28d3 : 0x10028d3, - 0x28d4 : 0x10028d4, - 0x28d5 : 0x10028d5, - 0x28d6 : 0x10028d6, - 0x28d7 : 0x10028d7, - 0x28d8 : 0x10028d8, - 0x28d9 : 0x10028d9, - 0x28da : 0x10028da, - 0x28db : 0x10028db, - 0x28dc : 0x10028dc, - 0x28dd : 0x10028dd, - 0x28de : 0x10028de, - 0x28df : 0x10028df, - 0x28e0 : 0x10028e0, - 0x28e1 : 0x10028e1, - 0x28e2 : 0x10028e2, - 0x28e3 : 0x10028e3, - 0x28e4 : 0x10028e4, - 0x28e5 : 0x10028e5, - 0x28e6 : 0x10028e6, - 0x28e7 : 0x10028e7, - 0x28e8 : 0x10028e8, - 0x28e9 : 0x10028e9, - 0x28ea : 0x10028ea, - 0x28eb : 0x10028eb, - 0x28ec : 0x10028ec, - 0x28ed : 0x10028ed, - 0x28ee : 0x10028ee, - 0x28ef : 0x10028ef, - 0x28f0 : 0x10028f0, - 0x28f1 : 0x10028f1, - 0x28f2 : 0x10028f2, - 0x28f3 : 0x10028f3, - 0x28f4 : 0x10028f4, - 0x28f5 : 0x10028f5, - 0x28f6 : 0x10028f6, - 0x28f7 : 0x10028f7, - 0x28f8 : 0x10028f8, - 0x28f9 : 0x10028f9, - 0x28fa : 0x10028fa, - 0x28fb : 0x10028fb, - 0x28fc : 0x10028fc, - 0x28fd : 0x10028fd, - 0x28fe : 0x10028fe, - 0x28ff : 0x10028ff -}; + ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release + ['onMouseMove', 'rw', 'func'], // Handler for mouse movement + ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + ]); +})(); diff --git a/webclients/novnc/include/jsunzip.js b/webclients/novnc/include/jsunzip.js index f815218..8968f86 100755 --- a/webclients/novnc/include/jsunzip.js +++ b/webclients/novnc/include/jsunzip.js @@ -352,20 +352,28 @@ this.getbit = function(d) } /* read a num bit value from a stream and add base */ +function read_bits_direct(source, bitcount, tag, idx, num) +{ + var val = 0; + while (bitcount < 24) { + tag = tag | (source[idx++] & 0xff) << bitcount; + bitcount += 8; + } + val = tag & (0xffff >> (16 - num)); + tag >>= num; + bitcount -= num; + return [bitcount, tag, idx, val]; +} this.read_bits = function(d, num, base) { if (!num) return base; - var val = 0; - while (d.bitcount < 24) { - d.tag = d.tag | (d.source[d.sourceIndex++] & 0xff) << d.bitcount; - d.bitcount += 8; - } - val = d.tag & (0xffff >> (16 - num)); - d.tag >>= num; - d.bitcount -= num; - return val + base; + var ret = read_bits_direct(d.source, d.bitcount, d.tag, d.sourceIndex, num); + d.bitcount = ret[0]; + d.tag = ret[1]; + d.sourceIndex = ret[2]; + return ret[3] + base; } /* given a data stream and a tree, decode a symbol */ diff --git a/webclients/novnc/include/keyboard.js b/webclients/novnc/include/keyboard.js new file mode 100644 index 0000000..6044321 --- /dev/null +++ b/webclients/novnc/include/keyboard.js @@ -0,0 +1,543 @@ +var kbdUtil = (function() { + "use strict"; + + function substituteCodepoint(cp) { + // Any Unicode code points which do not have corresponding keysym entries + // can be swapped out for another code point by adding them to this table + var substitutions = { + // {S,s} with comma below -> {S,s} with cedilla + 0x218 : 0x15e, + 0x219 : 0x15f, + // {T,t} with comma below -> {T,t} with cedilla + 0x21a : 0x162, + 0x21b : 0x163 + }; + + var sub = substitutions[cp]; + return sub ? sub : cp; + } + + function isMac() { + return navigator && !!(/mac/i).exec(navigator.platform); + } + function isWindows() { + return navigator && !!(/win/i).exec(navigator.platform); + } + function isLinux() { + return navigator && !!(/linux/i).exec(navigator.platform); + } + + // Return true if a modifier which is not the specified char modifier (and is not shift) is down + function hasShortcutModifier(charModifier, currentModifiers) { + var mods = {}; + for (var key in currentModifiers) { + if (parseInt(key) !== 0xffe1) { + mods[key] = currentModifiers[key]; + } + } + + var sum = 0; + for (var k in currentModifiers) { + if (mods[k]) { + ++sum; + } + } + if (hasCharModifier(charModifier, mods)) { + return sum > charModifier.length; + } + else { + return sum > 0; + } + } + + // Return true if the specified char modifier is currently down + function hasCharModifier(charModifier, currentModifiers) { + if (charModifier.length === 0) { return false; } + + for (var i = 0; i < charModifier.length; ++i) { + if (!currentModifiers[charModifier[i]]) { + return false; + } + } + return true; + } + + // Helper object tracking modifier key state + // and generates fake key events to compensate if it gets out of sync + function ModifierSync(charModifier) { + var ctrl = 0xffe3; + var alt = 0xffe9; + var altGr = 0xfe03; + var shift = 0xffe1; + var meta = 0xffe7; + + if (!charModifier) { + if (isMac()) { + // on Mac, Option (AKA Alt) is used as a char modifier + charModifier = [alt]; + } + else if (isWindows()) { + // on Windows, Ctrl+Alt is used as a char modifier + charModifier = [alt, ctrl]; + } + else if (isLinux()) { + // on Linux, AltGr is used as a char modifier + charModifier = [altGr]; + } + else { + charModifier = []; + } + } + + var state = {}; + state[ctrl] = false; + state[alt] = false; + state[altGr] = false; + state[shift] = false; + state[meta] = false; + + function sync(evt, keysym) { + var result = []; + function syncKey(keysym) { + return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; + } + + if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) { + state[ctrl] = evt.ctrlKey; + result.push(syncKey(ctrl)); + } + if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) { + state[alt] = evt.altKey; + result.push(syncKey(alt)); + } + if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) { + state[altGr] = evt.altGraphKey; + result.push(syncKey(altGr)); + } + if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) { + state[shift] = evt.shiftKey; + result.push(syncKey(shift)); + } + if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) { + state[meta] = evt.metaKey; + result.push(syncKey(meta)); + } + return result; + } + function syncKeyEvent(evt, down) { + var obj = getKeysym(evt); + var keysym = obj ? obj.keysym : null; + + // first, apply the event itself, if relevant + if (keysym !== null && state[keysym] !== undefined) { + state[keysym] = down; + } + return sync(evt, keysym); + } + + return { + // sync on the appropriate keyboard event + keydown: function(evt) { return syncKeyEvent(evt, true);}, + keyup: function(evt) { return syncKeyEvent(evt, false);}, + // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway + syncAny: function(evt) { return sync(evt);}, + + // is a shortcut modifier down? + hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, + // if a char modifier is down, return the keys it consists of, otherwise return null + activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } + }; + } + + // Get a key ID from a keyboard event + // May be a string or an integer depending on the available properties + function getKey(evt){ + if ('keyCode' in evt && 'key' in evt) { + return evt.key + ':' + evt.keyCode; + } + else if ('keyCode' in evt) { + return evt.keyCode; + } + else { + return evt.key; + } + } + + // Get the most reliable keysym value we can get from a key event + // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which + function getKeysym(evt){ + var codepoint; + if (evt.char && evt.char.length === 1) { + codepoint = evt.char.charCodeAt(); + } + else if (evt.charCode) { + codepoint = evt.charCode; + } + else if (evt.keyCode && evt.type === 'keypress') { + // IE10 stores the char code as keyCode, and has no other useful properties + codepoint = evt.keyCode; + } + if (codepoint) { + var res = keysyms.fromUnicode(substituteCodepoint(codepoint)); + if (res) { + return res; + } + } + // we could check evt.key here. + // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, + // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key + // so we don't *need* it yet + if (evt.keyCode) { + return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); + } + if (evt.which) { + return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); + } + return null; + } + + // Given a keycode, try to predict which keysym it might be. + // If the keycode is unknown, null is returned. + function keysymFromKeyCode(keycode, shiftPressed) { + if (typeof(keycode) !== 'number') { + return null; + } + // won't be accurate for azerty + if (keycode >= 0x30 && keycode <= 0x39) { + return keycode; // digit + } + if (keycode >= 0x41 && keycode <= 0x5a) { + // remap to lowercase unless shift is down + return shiftPressed ? keycode : keycode + 32; // A-Z + } + if (keycode >= 0x60 && keycode <= 0x69) { + return 0xffb0 + (keycode - 0x60); // numpad 0-9 + } + + switch(keycode) { + case 0x20: return 0x20; // space + case 0x6a: return 0xffaa; // multiply + case 0x6b: return 0xffab; // add + case 0x6c: return 0xffac; // separator + case 0x6d: return 0xffad; // subtract + case 0x6e: return 0xffae; // decimal + case 0x6f: return 0xffaf; // divide + case 0xbb: return 0x2b; // + + case 0xbc: return 0x2c; // , + case 0xbd: return 0x2d; // - + case 0xbe: return 0x2e; // . + } + + return nonCharacterKey({keyCode: keycode}); + } + + // if the key is a known non-character key (any key which doesn't generate character data) + // return its keysym value. Otherwise return null + function nonCharacterKey(evt) { + // evt.key not implemented yet + if (!evt.keyCode) { return null; } + var keycode = evt.keyCode; + + if (keycode >= 0x70 && keycode <= 0x87) { + return 0xffbe + keycode - 0x70; // F1-F24 + } + switch (keycode) { + + case 8 : return 0xFF08; // BACKSPACE + case 13 : return 0xFF0D; // ENTER + + case 9 : return 0xFF09; // TAB + + case 27 : return 0xFF1B; // ESCAPE + case 46 : return 0xFFFF; // DELETE + + case 36 : return 0xFF50; // HOME + case 35 : return 0xFF57; // END + case 33 : return 0xFF55; // PAGE_UP + case 34 : return 0xFF56; // PAGE_DOWN + case 45 : return 0xFF63; // INSERT + + case 37 : return 0xFF51; // LEFT + case 38 : return 0xFF52; // UP + case 39 : return 0xFF53; // RIGHT + case 40 : return 0xFF54; // DOWN + case 16 : return 0xFFE1; // SHIFT + case 17 : return 0xFFE3; // CONTROL + case 18 : return 0xFFE9; // Left ALT (Mac Option) + + case 224 : return 0xFE07; // Meta + case 225 : return 0xFE03; // AltGr + case 91 : return 0xFFEC; // Super_L (Win Key) + case 92 : return 0xFFED; // Super_R (Win Key) + case 93 : return 0xFF67; // Menu (Win Menu), Mac Command + default: return null; + } + } + return { + hasShortcutModifier : hasShortcutModifier, + hasCharModifier : hasCharModifier, + ModifierSync : ModifierSync, + getKey : getKey, + getKeysym : getKeysym, + keysymFromKeyCode : keysymFromKeyCode, + nonCharacterKey : nonCharacterKey, + substituteCodepoint : substituteCodepoint + }; +})(); + +// Takes a DOM keyboard event and: +// - determines which keysym it represents +// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) +// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down +// - marks each event with an 'escape' property if a modifier was down which should be "escaped" +// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown +// This information is collected into an object which is passed to the next() function. (one call per event) +function KeyEventDecoder(modifierState, next) { + "use strict"; + function sendAll(evts) { + for (var i = 0; i < evts.length; ++i) { + next(evts[i]); + } + } + function process(evt, type) { + var result = {type: type}; + var keyId = kbdUtil.getKey(evt); + if (keyId) { + result.keyId = keyId; + } + + var keysym = kbdUtil.getKeysym(evt); + + var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); + // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? + // "special" keys like enter, tab or backspace don't send keypress events, + // and some browsers don't send keypresses at all if a modifier is down + if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) { + result.keysym = keysym; + } + + var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; + + // Should we prevent the browser from handling the event? + // Doing so on a keydown (in most browsers) prevents keypress from being generated + // so only do that if we have to. + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt)); + + // If a char modifier is down on a keydown, we need to insert a stall, + // so VerifyCharModifier knows to wait and see if a keypress is comnig + var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt); + + // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) + var active = modifierState.activeCharModifier(); + + // If we have a char modifier down, and we're able to determine a keysym reliably + // then (a) we know to treat the modifier as a char modifier, + // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char. + if (active && keysym) { + var isCharModifier = false; + for (var i = 0; i < active.length; ++i) { + if (active[i] === keysym.keysym) { + isCharModifier = true; + } + } + if (type === 'keypress' && !isCharModifier) { + result.escape = modifierState.activeCharModifier(); + } + } + + if (stall) { + // insert a fake "stall" event + next({type: 'stall'}); + } + next(result); + + return suppress; + } + + return { + keydown: function(evt) { + sendAll(modifierState.keydown(evt)); + return process(evt, 'keydown'); + }, + keypress: function(evt) { + return process(evt, 'keypress'); + }, + keyup: function(evt) { + sendAll(modifierState.keyup(evt)); + return process(evt, 'keyup'); + }, + syncModifiers: function(evt) { + sendAll(modifierState.syncAny(evt)); + }, + releaseAll: function() { next({type: 'releaseall'}); } + }; +} + +// Combines keydown and keypress events where necessary to handle char modifiers. +// On some OS'es, a char modifier is sometimes used as a shortcut modifier. +// For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing +// so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not. +// The only way we can distinguish these cases is to wait and see if a keypress event arrives +// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two +function VerifyCharModifier(next) { + "use strict"; + var queue = []; + var timer = null; + function process() { + if (timer) { + return; + } + + var delayProcess = function () { + clearTimeout(timer); + timer = null; + process(); + }; + + while (queue.length !== 0) { + var cur = queue[0]; + queue = queue.splice(1); + switch (cur.type) { + case 'stall': + // insert a delay before processing available events. + /* jshint loopfunc: true */ + timer = setTimeout(delayProcess, 5); + /* jshint loopfunc: false */ + return; + case 'keydown': + // is the next element a keypress? Then we should merge the two + if (queue.length !== 0 && queue[0].type === 'keypress') { + // Firefox sends keypress even when no char is generated. + // so, if keypress keysym is the same as we'd have guessed from keydown, + // the modifier didn't have any effect, and should not be escaped + if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) { + cur.escape = queue[0].escape; + } + cur.keysym = queue[0].keysym; + queue = queue.splice(1); + } + break; + } + + // swallow stall events, and pass all others to the next stage + if (cur.type !== 'stall') { + next(cur); + } + } + } + return function(evt) { + queue.push(evt); + process(); + }; +} + +// Keeps track of which keys we (and the server) believe are down +// When a keyup is received, match it against this list, to determine the corresponding keysym(s) +// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars +// key repeat events should be merged into a single entry. +// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess +function TrackKeyState(next) { + "use strict"; + var state = []; + + return function (evt) { + var last = state.length !== 0 ? state[state.length-1] : null; + + switch (evt.type) { + case 'keydown': + // insert a new entry if last seen key was different. + if (!last || !evt.keyId || last.keyId !== evt.keyId) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (evt.keysym) { + // make sure last event contains this keysym (a single "logical" keyevent + // can cause multiple key events to be sent to the VNC server) + last.keysyms[evt.keysym.keysym] = evt.keysym; + last.ignoreKeyPress = true; + next(evt); + } + break; + case 'keypress': + if (!last) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (!evt.keysym) { + console.log('keypress with no keysym:', evt); + } + + // If we didn't expect a keypress, and already sent a keydown to the VNC server + // based on the keydown, make sure to skip this event. + if (evt.keysym && !last.ignoreKeyPress) { + last.keysyms[evt.keysym.keysym] = evt.keysym; + evt.type = 'keydown'; + next(evt); + } + break; + case 'keyup': + if (state.length === 0) { + return; + } + var idx = null; + // do we have a matching key tracked as being down? + for (var i = 0; i !== state.length; ++i) { + if (state[i].keyId === evt.keyId) { + idx = i; + break; + } + } + // if we couldn't find a match (it happens), assume it was the last key pressed + if (idx === null) { + idx = state.length - 1; + } + + var item = state.splice(idx, 1)[0]; + // for each keysym tracked by this key entry, clone the current event and override the keysym + var clone = (function(){ + function Clone(){} + return function (obj) { Clone.prototype=obj; return new Clone(); }; + }()); + for (var key in item.keysyms) { + var out = clone(evt); + out.keysym = item.keysyms[key]; + next(out); + } + break; + case 'releaseall': + /* jshint shadow: true */ + for (var i = 0; i < state.length; ++i) { + for (var key in state[i].keysyms) { + var keysym = state[i].keysyms[key]; + next({keyId: 0, keysym: keysym, type: 'keyup'}); + } + } + /* jshint shadow: false */ + state = []; + } + }; +} + +// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), +// then the modifier must be "undone" before sending the @, and "redone" afterwards. +function EscapeModifiers(next) { + "use strict"; + return function(evt) { + if (evt.type !== 'keydown' || evt.escape === undefined) { + next(evt); + return; + } + // undo modifiers + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + // send the character event + next(evt); + // redo modifiers + /* jshint shadow: true */ + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + /* jshint shadow: false */ + }; +} diff --git a/webclients/novnc/include/keysym.js b/webclients/novnc/include/keysym.js new file mode 100644 index 0000000..a00d595 --- /dev/null +++ b/webclients/novnc/include/keysym.js @@ -0,0 +1,376 @@ +var XK_VoidSymbol = 0xffffff, /* Void symbol */ + +XK_BackSpace = 0xff08, /* Back space, back char */ +XK_Tab = 0xff09, +XK_Linefeed = 0xff0a, /* Linefeed, LF */ +XK_Clear = 0xff0b, +XK_Return = 0xff0d, /* Return, enter */ +XK_Pause = 0xff13, /* Pause, hold */ +XK_Scroll_Lock = 0xff14, +XK_Sys_Req = 0xff15, +XK_Escape = 0xff1b, +XK_Delete = 0xffff, /* Delete, rubout */ + +/* Cursor control & motion */ + +XK_Home = 0xff50, +XK_Left = 0xff51, /* Move left, left arrow */ +XK_Up = 0xff52, /* Move up, up arrow */ +XK_Right = 0xff53, /* Move right, right arrow */ +XK_Down = 0xff54, /* Move down, down arrow */ +XK_Prior = 0xff55, /* Prior, previous */ +XK_Page_Up = 0xff55, +XK_Next = 0xff56, /* Next */ +XK_Page_Down = 0xff56, +XK_End = 0xff57, /* EOL */ +XK_Begin = 0xff58, /* BOL */ + + +/* Misc functions */ + +XK_Select = 0xff60, /* Select, mark */ +XK_Print = 0xff61, +XK_Execute = 0xff62, /* Execute, run, do */ +XK_Insert = 0xff63, /* Insert, insert here */ +XK_Undo = 0xff65, +XK_Redo = 0xff66, /* Redo, again */ +XK_Menu = 0xff67, +XK_Find = 0xff68, /* Find, search */ +XK_Cancel = 0xff69, /* Cancel, stop, abort, exit */ +XK_Help = 0xff6a, /* Help */ +XK_Break = 0xff6b, +XK_Mode_switch = 0xff7e, /* Character set switch */ +XK_script_switch = 0xff7e, /* Alias for mode_switch */ +XK_Num_Lock = 0xff7f, + +/* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ + +XK_KP_Space = 0xff80, /* Space */ +XK_KP_Tab = 0xff89, +XK_KP_Enter = 0xff8d, /* Enter */ +XK_KP_F1 = 0xff91, /* PF1, KP_A, ... */ +XK_KP_F2 = 0xff92, +XK_KP_F3 = 0xff93, +XK_KP_F4 = 0xff94, +XK_KP_Home = 0xff95, +XK_KP_Left = 0xff96, +XK_KP_Up = 0xff97, +XK_KP_Right = 0xff98, +XK_KP_Down = 0xff99, +XK_KP_Prior = 0xff9a, +XK_KP_Page_Up = 0xff9a +XK_KP_Next = 0xff9b, +XK_KP_Page_Down = 0xff9b, +XK_KP_End = 0xff9c, +XK_KP_Begin = 0xff9d, +XK_KP_Insert = 0xff9e, +XK_KP_Delete = 0xff9f, +XK_KP_Equal = 0xffbd, /* Equals */ +XK_KP_Multiply = 0xffaa, +XK_KP_Add = 0xffab, +XK_KP_Separator = 0xffac, /* Separator, often comma */ +XK_KP_Subtract = 0xffad, +XK_KP_Decimal = 0xffae, +XK_KP_Divide = 0xffaf, + +XK_KP_0 = 0xffb0, +XK_KP_1 = 0xffb1, +XK_KP_2 = 0xffb2, +XK_KP_3 = 0xffb3, +XK_KP_4 = 0xffb4, +XK_KP_5 = 0xffb5, +XK_KP_6 = 0xffb6, +XK_KP_7 = 0xffb7, +XK_KP_8 = 0xffb8, +XK_KP_9 = 0xffb9, + +/* + * Auxiliary functions; note the duplicate definitions for left and right + * function keys; Sun keyboards and a few other manufacturers have such + * function key groups on the left and/or right sides of the keyboard. + * We've not found a keyboard with more than 35 function keys total. + */ + +XK_F1 = 0xffbe, +XK_F2 = 0xffbf, +XK_F3 = 0xffc0, +XK_F4 = 0xffc1, +XK_F5 = 0xffc2, +XK_F6 = 0xffc3, +XK_F7 = 0xffc4, +XK_F8 = 0xffc5, +XK_F9 = 0xffc6, +XK_F10 = 0xffc7, +XK_F11 = 0xffc8, +XK_L1 = 0xffc8, +XK_F12 = 0xffc9, +XK_L2 = 0xffc9, +XK_F13 = 0xffca, +XK_L3 = 0xffca, +XK_F14 = 0xffcb, +XK_L4 = 0xffcb, +XK_F15 = 0xffcc, +XK_L5 = 0xffcc, +XK_F16 = 0xffcd, +XK_L6 = 0xffcd, +XK_F17 = 0xffce, +XK_L7 = 0xffce, +XK_F18 = 0xffcf, +XK_L8 = 0xffcf, +XK_F19 = 0xffd0, +XK_L9 = 0xffd0, +XK_F20 = 0xffd1, +XK_L10 = 0xffd1, +XK_F21 = 0xffd2, +XK_R1 = 0xffd2, +XK_F22 = 0xffd3, +XK_R2 = 0xffd3, +XK_F23 = 0xffd4, +XK_R3 = 0xffd4, +XK_F24 = 0xffd5, +XK_R4 = 0xffd5, +XK_F25 = 0xffd6, +XK_R5 = 0xffd6, +XK_F26 = 0xffd7, +XK_R6 = 0xffd7, +XK_F27 = 0xffd8, +XK_R7 = 0xffd8, +XK_F28 = 0xffd9, +XK_R8 = 0xffd9, +XK_F29 = 0xffda, +XK_R9 = 0xffda, +XK_F30 = 0xffdb, +XK_R10 = 0xffdb, +XK_F31 = 0xffdc, +XK_R11 = 0xffdc, +XK_F32 = 0xffdd, +XK_R12 = 0xffdd, +XK_F33 = 0xffde, +XK_R13 = 0xffde, +XK_F34 = 0xffdf, +XK_R14 = 0xffdf, +XK_F35 = 0xffe0, +XK_R15 = 0xffe0, + +/* Modifiers */ + +XK_Shift_L = 0xffe1, /* Left shift */ +XK_Shift_R = 0xffe2, /* Right shift */ +XK_Control_L = 0xffe3, /* Left control */ +XK_Control_R = 0xffe4, /* Right control */ +XK_Caps_Lock = 0xffe5, /* Caps lock */ +XK_Shift_Lock = 0xffe6, /* Shift lock */ + +XK_Meta_L = 0xffe7, /* Left meta */ +XK_Meta_R = 0xffe8, /* Right meta */ +XK_Alt_L = 0xffe9, /* Left alt */ +XK_Alt_R = 0xffea, /* Right alt */ +XK_Super_L = 0xffeb, /* Left super */ +XK_Super_R = 0xffec, /* Right super */ +XK_Hyper_L = 0xffed, /* Left hyper */ +XK_Hyper_R = 0xffee, /* Right hyper */ + +/* + * Latin 1 + * (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) + * Byte 3 = 0 + */ + +XK_space = 0x0020, /* U+0020 SPACE */ +XK_exclam = 0x0021, /* U+0021 EXCLAMATION MARK */ +XK_quotedbl = 0x0022, /* U+0022 QUOTATION MARK */ +XK_numbersign = 0x0023, /* U+0023 NUMBER SIGN */ +XK_dollar = 0x0024, /* U+0024 DOLLAR SIGN */ +XK_percent = 0x0025, /* U+0025 PERCENT SIGN */ +XK_ampersand = 0x0026, /* U+0026 AMPERSAND */ +XK_apostrophe = 0x0027, /* U+0027 APOSTROPHE */ +XK_quoteright = 0x0027, /* deprecated */ +XK_parenleft = 0x0028, /* U+0028 LEFT PARENTHESIS */ +XK_parenright = 0x0029, /* U+0029 RIGHT PARENTHESIS */ +XK_asterisk = 0x002a, /* U+002A ASTERISK */ +XK_plus = 0x002b, /* U+002B PLUS SIGN */ +XK_comma = 0x002c, /* U+002C COMMA */ +XK_minus = 0x002d, /* U+002D HYPHEN-MINUS */ +XK_period = 0x002e, /* U+002E FULL STOP */ +XK_slash = 0x002f, /* U+002F SOLIDUS */ +XK_0 = 0x0030, /* U+0030 DIGIT ZERO */ +XK_1 = 0x0031, /* U+0031 DIGIT ONE */ +XK_2 = 0x0032, /* U+0032 DIGIT TWO */ +XK_3 = 0x0033, /* U+0033 DIGIT THREE */ +XK_4 = 0x0034, /* U+0034 DIGIT FOUR */ +XK_5 = 0x0035, /* U+0035 DIGIT FIVE */ +XK_6 = 0x0036, /* U+0036 DIGIT SIX */ +XK_7 = 0x0037, /* U+0037 DIGIT SEVEN */ +XK_8 = 0x0038, /* U+0038 DIGIT EIGHT */ +XK_9 = 0x0039, /* U+0039 DIGIT NINE */ +XK_colon = 0x003a, /* U+003A COLON */ +XK_semicolon = 0x003b, /* U+003B SEMICOLON */ +XK_less = 0x003c, /* U+003C LESS-THAN SIGN */ +XK_equal = 0x003d, /* U+003D EQUALS SIGN */ +XK_greater = 0x003e, /* U+003E GREATER-THAN SIGN */ +XK_question = 0x003f, /* U+003F QUESTION MARK */ +XK_at = 0x0040, /* U+0040 COMMERCIAL AT */ +XK_A = 0x0041, /* U+0041 LATIN CAPITAL LETTER A */ +XK_B = 0x0042, /* U+0042 LATIN CAPITAL LETTER B */ +XK_C = 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ +XK_D = 0x0044, /* U+0044 LATIN CAPITAL LETTER D */ +XK_E = 0x0045, /* U+0045 LATIN CAPITAL LETTER E */ +XK_F = 0x0046, /* U+0046 LATIN CAPITAL LETTER F */ +XK_G = 0x0047, /* U+0047 LATIN CAPITAL LETTER G */ +XK_H = 0x0048, /* U+0048 LATIN CAPITAL LETTER H */ +XK_I = 0x0049, /* U+0049 LATIN CAPITAL LETTER I */ +XK_J = 0x004a, /* U+004A LATIN CAPITAL LETTER J */ +XK_K = 0x004b, /* U+004B LATIN CAPITAL LETTER K */ +XK_L = 0x004c, /* U+004C LATIN CAPITAL LETTER L */ +XK_M = 0x004d, /* U+004D LATIN CAPITAL LETTER M */ +XK_N = 0x004e, /* U+004E LATIN CAPITAL LETTER N */ +XK_O = 0x004f, /* U+004F LATIN CAPITAL LETTER O */ +XK_P = 0x0050, /* U+0050 LATIN CAPITAL LETTER P */ +XK_Q = 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */ +XK_R = 0x0052, /* U+0052 LATIN CAPITAL LETTER R */ +XK_S = 0x0053, /* U+0053 LATIN CAPITAL LETTER S */ +XK_T = 0x0054, /* U+0054 LATIN CAPITAL LETTER T */ +XK_U = 0x0055, /* U+0055 LATIN CAPITAL LETTER U */ +XK_V = 0x0056, /* U+0056 LATIN CAPITAL LETTER V */ +XK_W = 0x0057, /* U+0057 LATIN CAPITAL LETTER W */ +XK_X = 0x0058, /* U+0058 LATIN CAPITAL LETTER X */ +XK_Y = 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */ +XK_Z = 0x005a, /* U+005A LATIN CAPITAL LETTER Z */ +XK_bracketleft = 0x005b, /* U+005B LEFT SQUARE BRACKET */ +XK_backslash = 0x005c, /* U+005C REVERSE SOLIDUS */ +XK_bracketright = 0x005d, /* U+005D RIGHT SQUARE BRACKET */ +XK_asciicircum = 0x005e, /* U+005E CIRCUMFLEX ACCENT */ +XK_underscore = 0x005f, /* U+005F LOW LINE */ +XK_grave = 0x0060, /* U+0060 GRAVE ACCENT */ +XK_quoteleft = 0x0060, /* deprecated */ +XK_a = 0x0061, /* U+0061 LATIN SMALL LETTER A */ +XK_b = 0x0062, /* U+0062 LATIN SMALL LETTER B */ +XK_c = 0x0063, /* U+0063 LATIN SMALL LETTER C */ +XK_d = 0x0064, /* U+0064 LATIN SMALL LETTER D */ +XK_e = 0x0065, /* U+0065 LATIN SMALL LETTER E */ +XK_f = 0x0066, /* U+0066 LATIN SMALL LETTER F */ +XK_g = 0x0067, /* U+0067 LATIN SMALL LETTER G */ +XK_h = 0x0068, /* U+0068 LATIN SMALL LETTER H */ +XK_i = 0x0069, /* U+0069 LATIN SMALL LETTER I */ +XK_j = 0x006a, /* U+006A LATIN SMALL LETTER J */ +XK_k = 0x006b, /* U+006B LATIN SMALL LETTER K */ +XK_l = 0x006c, /* U+006C LATIN SMALL LETTER L */ +XK_m = 0x006d, /* U+006D LATIN SMALL LETTER M */ +XK_n = 0x006e, /* U+006E LATIN SMALL LETTER N */ +XK_o = 0x006f, /* U+006F LATIN SMALL LETTER O */ +XK_p = 0x0070, /* U+0070 LATIN SMALL LETTER P */ +XK_q = 0x0071, /* U+0071 LATIN SMALL LETTER Q */ +XK_r = 0x0072, /* U+0072 LATIN SMALL LETTER R */ +XK_s = 0x0073, /* U+0073 LATIN SMALL LETTER S */ +XK_t = 0x0074, /* U+0074 LATIN SMALL LETTER T */ +XK_u = 0x0075, /* U+0075 LATIN SMALL LETTER U */ +XK_v = 0x0076, /* U+0076 LATIN SMALL LETTER V */ +XK_w = 0x0077, /* U+0077 LATIN SMALL LETTER W */ +XK_x = 0x0078, /* U+0078 LATIN SMALL LETTER X */ +XK_y = 0x0079, /* U+0079 LATIN SMALL LETTER Y */ +XK_z = 0x007a, /* U+007A LATIN SMALL LETTER Z */ +XK_braceleft = 0x007b, /* U+007B LEFT CURLY BRACKET */ +XK_bar = 0x007c, /* U+007C VERTICAL LINE */ +XK_braceright = 0x007d, /* U+007D RIGHT CURLY BRACKET */ +XK_asciitilde = 0x007e, /* U+007E TILDE */ + +XK_nobreakspace = 0x00a0, /* U+00A0 NO-BREAK SPACE */ +XK_exclamdown = 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */ +XK_cent = 0x00a2, /* U+00A2 CENT SIGN */ +XK_sterling = 0x00a3, /* U+00A3 POUND SIGN */ +XK_currency = 0x00a4, /* U+00A4 CURRENCY SIGN */ +XK_yen = 0x00a5, /* U+00A5 YEN SIGN */ +XK_brokenbar = 0x00a6, /* U+00A6 BROKEN BAR */ +XK_section = 0x00a7, /* U+00A7 SECTION SIGN */ +XK_diaeresis = 0x00a8, /* U+00A8 DIAERESIS */ +XK_copyright = 0x00a9, /* U+00A9 COPYRIGHT SIGN */ +XK_ordfeminine = 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */ +XK_guillemotleft = 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_notsign = 0x00ac, /* U+00AC NOT SIGN */ +XK_hyphen = 0x00ad, /* U+00AD SOFT HYPHEN */ +XK_registered = 0x00ae, /* U+00AE REGISTERED SIGN */ +XK_macron = 0x00af, /* U+00AF MACRON */ +XK_degree = 0x00b0, /* U+00B0 DEGREE SIGN */ +XK_plusminus = 0x00b1, /* U+00B1 PLUS-MINUS SIGN */ +XK_twosuperior = 0x00b2, /* U+00B2 SUPERSCRIPT TWO */ +XK_threesuperior = 0x00b3, /* U+00B3 SUPERSCRIPT THREE */ +XK_acute = 0x00b4, /* U+00B4 ACUTE ACCENT */ +XK_mu = 0x00b5, /* U+00B5 MICRO SIGN */ +XK_paragraph = 0x00b6, /* U+00B6 PILCROW SIGN */ +XK_periodcentered = 0x00b7, /* U+00B7 MIDDLE DOT */ +XK_cedilla = 0x00b8, /* U+00B8 CEDILLA */ +XK_onesuperior = 0x00b9, /* U+00B9 SUPERSCRIPT ONE */ +XK_masculine = 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */ +XK_guillemotright = 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_onequarter = 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */ +XK_onehalf = 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */ +XK_threequarters = 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */ +XK_questiondown = 0x00bf, /* U+00BF INVERTED QUESTION MARK */ +XK_Agrave = 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */ +XK_Aacute = 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */ +XK_Acircumflex = 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ +XK_Atilde = 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */ +XK_Adiaeresis = 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */ +XK_Aring = 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */ +XK_AE = 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */ +XK_Ccedilla = 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */ +XK_Egrave = 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */ +XK_Eacute = 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */ +XK_Ecircumflex = 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ +XK_Ediaeresis = 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */ +XK_Igrave = 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */ +XK_Iacute = 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */ +XK_Icircumflex = 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ +XK_Idiaeresis = 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */ +XK_ETH = 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */ +XK_Eth = 0x00d0, /* deprecated */ +XK_Ntilde = 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */ +XK_Ograve = 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */ +XK_Oacute = 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */ +XK_Ocircumflex = 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ +XK_Otilde = 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */ +XK_Odiaeresis = 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */ +XK_multiply = 0x00d7, /* U+00D7 MULTIPLICATION SIGN */ +XK_Oslash = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ooblique = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ugrave = 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */ +XK_Uacute = 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */ +XK_Ucircumflex = 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ +XK_Udiaeresis = 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */ +XK_Yacute = 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */ +XK_THORN = 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */ +XK_Thorn = 0x00de, /* deprecated */ +XK_ssharp = 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */ +XK_agrave = 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */ +XK_aacute = 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */ +XK_acircumflex = 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */ +XK_atilde = 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */ +XK_adiaeresis = 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */ +XK_aring = 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */ +XK_ae = 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */ +XK_ccedilla = 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */ +XK_egrave = 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */ +XK_eacute = 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */ +XK_ecircumflex = 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */ +XK_ediaeresis = 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */ +XK_igrave = 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */ +XK_iacute = 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */ +XK_icircumflex = 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */ +XK_idiaeresis = 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */ +XK_eth = 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */ +XK_ntilde = 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */ +XK_ograve = 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */ +XK_oacute = 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */ +XK_ocircumflex = 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */ +XK_otilde = 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */ +XK_odiaeresis = 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */ +XK_division = 0x00f7, /* U+00F7 DIVISION SIGN */ +XK_oslash = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ooblique = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ugrave = 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */ +XK_uacute = 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */ +XK_ucircumflex = 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */ +XK_udiaeresis = 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */ +XK_yacute = 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ +XK_thorn = 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ +XK_ydiaeresis = 0x00ff; /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ diff --git a/webclients/novnc/include/keysymdef.js b/webclients/novnc/include/keysymdef.js new file mode 100644 index 0000000..f94445c --- /dev/null +++ b/webclients/novnc/include/keysymdef.js @@ -0,0 +1,15 @@ +// This file describes mappings from Unicode codepoints to the keysym values +// (and optionally, key names) expected by the RFB protocol +// How this file was generated: +// node /Users/jalf/dev/mi/novnc/utils/parse.js /opt/X11/include/X11/keysymdef.h +var keysyms = (function(){ + "use strict"; + var keynames = null; + var codepoints = {"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"49":49,"50":50,"51":51,"52":52,"53":53,"54":54,"55":55,"56":56,"57":57,"58":58,"59":59,"60":60,"61":61,"62":62,"63":63,"64":64,"65":65,"66":66,"67":67,"68":68,"69":69,"70":70,"71":71,"72":72,"73":73,"74":74,"75":75,"76":76,"77":77,"78":78,"79":79,"80":80,"81":81,"82":82,"83":83,"84":84,"85":85,"86":86,"87":87,"88":88,"89":89,"90":90,"91":91,"92":92,"93":93,"94":94,"95":95,"96":96,"97":97,"98":98,"99":99,"100":100,"101":101,"102":102,"103":103,"104":104,"105":105,"106":106,"107":107,"108":108,"109":109,"110":110,"111":111,"112":112,"113":113,"114":114,"115":115,"116":116,"117":117,"118":118,"119":119,"120":120,"121":121,"122":122,"123":123,"124":124,"125":125,"126":126,"160":160,"161":161,"162":162,"163":163,"164":164,"165":165,"166":166,"167":167,"168":168,"169":169,"170":170,"171":171,"172":172,"173":173,"174":174,"175":175,"176":176,"177":177,"178":178,"179":179,"180":180,"181":181,"182":182,"183":183,"184":184,"185":185,"186":186,"187":187,"188":188,"189":189,"190":190,"191":191,"192":192,"193":193,"194":194,"195":195,"196":196,"197":197,"198":198,"199":199,"200":200,"201":201,"202":202,"203":203,"204":204,"205":205,"206":206,"207":207,"208":208,"209":209,"210":210,"211":211,"212":212,"213":213,"214":214,"215":215,"216":216,"217":217,"218":218,"219":219,"220":220,"221":221,"222":222,"223":223,"224":224,"225":225,"226":226,"227":227,"228":228,"229":229,"230":230,"231":231,"232":232,"233":233,"234":234,"235":235,"236":236,"237":237,"238":238,"239":239,"240":240,"241":241,"242":242,"243":243,"244":244,"245":245,"246":246,"247":247,"248":248,"249":249,"250":250,"251":251,"252":252,"253":253,"254":254,"255":255,"256":960,"257":992,"258":451,"259":483,"260":417,"261":433,"262":454,"263":486,"264":710,"265":742,"266":709,"267":741,"268":456,"269":488,"270":463,"271":495,"272":464,"273":496,"274":938,"275":954,"278":972,"279":1004,"280":458,"281":490,"282":460,"283":492,"284":728,"285":760,"286":683,"287":699,"288":725,"289":757,"290":939,"291":955,"292":678,"293":694,"294":673,"295":689,"296":933,"297":949,"298":975,"299":1007,"300":16777516,"301":16777517,"302":967,"303":999,"304":681,"305":697,"308":684,"309":700,"310":979,"311":1011,"312":930,"313":453,"314":485,"315":934,"316":950,"317":421,"318":437,"321":419,"322":435,"323":465,"324":497,"325":977,"326":1009,"327":466,"328":498,"330":957,"331":959,"332":978,"333":1010,"336":469,"337":501,"338":5052,"339":5053,"340":448,"341":480,"342":931,"343":947,"344":472,"345":504,"346":422,"347":438,"348":734,"349":766,"350":426,"351":442,"352":425,"353":441,"354":478,"355":510,"356":427,"357":443,"358":940,"359":956,"360":989,"361":1021,"362":990,"363":1022,"364":733,"365":765,"366":473,"367":505,"368":475,"369":507,"370":985,"371":1017,"372":16777588,"373":16777589,"374":16777590,"375":16777591,"376":5054,"377":428,"378":444,"379":431,"380":447,"381":430,"382":446,"399":16777615,"402":2294,"415":16777631,"416":16777632,"417":16777633,"431":16777647,"432":16777648,"437":16777653,"438":16777654,"439":16777655,"466":16777681,"486":16777702,"487":16777703,"601":16777817,"629":16777845,"658":16777874,"711":439,"728":418,"729":511,"731":434,"733":445,"901":1966,"902":1953,"904":1954,"905":1955,"906":1956,"908":1959,"910":1960,"911":1963,"912":1974,"913":1985,"914":1986,"915":1987,"916":1988,"917":1989,"918":1990,"919":1991,"920":1992,"921":1993,"922":1994,"923":1995,"924":1996,"925":1997,"926":1998,"927":1999,"928":2000,"929":2001,"931":2002,"932":2004,"933":2005,"934":2006,"935":2007,"936":2008,"937":2009,"938":1957,"939":1961,"940":1969,"941":1970,"942":1971,"943":1972,"944":1978,"945":2017,"946":2018,"947":2019,"948":2020,"949":2021,"950":2022,"951":2023,"952":2024,"953":2025,"954":2026,"955":2027,"956":2028,"957":2029,"958":2030,"959":2031,"960":2032,"961":2033,"962":2035,"963":2034,"964":2036,"965":2037,"966":2038,"967":2039,"968":2040,"969":2041,"970":1973,"971":1977,"972":1975,"973":1976,"974":1979,"1025":1715,"1026":1713,"1027":1714,"1028":1716,"1029":1717,"1030":1718,"1031":1719,"1032":1720,"1033":1721,"1034":1722,"1035":1723,"1036":1724,"1038":1726,"1039":1727,"1040":1761,"1041":1762,"1042":1783,"1043":1767,"1044":1764,"1045":1765,"1046":1782,"1047":1786,"1048":1769,"1049":1770,"1050":1771,"1051":1772,"1052":1773,"1053":1774,"1054":1775,"1055":1776,"1056":1778,"1057":1779,"1058":1780,"1059":1781,"1060":1766,"1061":1768,"1062":1763,"1063":1790,"1064":1787,"1065":1789,"1066":1791,"1067":1785,"1068":1784,"1069":1788,"1070":1760,"1071":1777,"1072":1729,"1073":1730,"1074":1751,"1075":1735,"1076":1732,"1077":1733,"1078":1750,"1079":1754,"1080":1737,"1081":1738,"1082":1739,"1083":1740,"1084":1741,"1085":1742,"1086":1743,"1087":1744,"1088":1746,"1089":1747,"1090":1748,"1091":1749,"1092":1734,"1093":1736,"1094":1731,"1095":1758,"1096":1755,"1097":1757,"1098":1759,"1099":1753,"1100":1752,"1101":1756,"1102":1728,"1103":1745,"1105":1699,"1106":1697,"1107":1698,"1108":1700,"1109":1701,"1110":1702,"1111":1703,"1112":1704,"1113":1705,"1114":1706,"1115":1707,"1116":1708,"1118":1710,"1119":1711,"1168":1725,"1169":1709,"1170":16778386,"1171":16778387,"1174":16778390,"1175":16778391,"1178":16778394,"1179":16778395,"1180":16778396,"1181":16778397,"1186":16778402,"1187":16778403,"1198":16778414,"1199":16778415,"1200":16778416,"1201":16778417,"1202":16778418,"1203":16778419,"1206":16778422,"1207":16778423,"1208":16778424,"1209":16778425,"1210":16778426,"1211":16778427,"1240":16778456,"1241":16778457,"1250":16778466,"1251":16778467,"1256":16778472,"1257":16778473,"1262":16778478,"1263":16778479,"1329":16778545,"1330":16778546,"1331":16778547,"1332":16778548,"1333":16778549,"1334":16778550,"1335":16778551,"1336":16778552,"1337":16778553,"1338":16778554,"1339":16778555,"1340":16778556,"1341":16778557,"1342":16778558,"1343":16778559,"1344":16778560,"1345":16778561,"1346":16778562,"1347":16778563,"1348":16778564,"1349":16778565,"1350":16778566,"1351":16778567,"1352":16778568,"1353":16778569,"1354":16778570,"1355":16778571,"1356":16778572,"1357":16778573,"1358":16778574,"1359":16778575,"1360":16778576,"1361":16778577,"1362":16778578,"1363":16778579,"1364":16778580,"1365":16778581,"1366":16778582,"1370":16778586,"1371":16778587,"1372":16778588,"1373":16778589,"1374":16778590,"1377":16778593,"1378":16778594,"1379":16778595,"1380":16778596,"1381":16778597,"1382":16778598,"1383":16778599,"1384":16778600,"1385":16778601,"1386":16778602,"1387":16778603,"1388":16778604,"1389":16778605,"1390":16778606,"1391":16778607,"1392":16778608,"1393":16778609,"1394":16778610,"1395":16778611,"1396":16778612,"1397":16778613,"1398":16778614,"1399":16778615,"1400":16778616,"1401":16778617,"1402":16778618,"1403":16778619,"1404":16778620,"1405":16778621,"1406":16778622,"1407":16778623,"1408":16778624,"1409":16778625,"1410":16778626,"1411":16778627,"1412":16778628,"1413":16778629,"1414":16778630,"1415":16778631,"1417":16778633,"1418":16778634,"1488":3296,"1489":3297,"1490":3298,"1491":3299,"1492":3300,"1493":3301,"1494":3302,"1495":3303,"1496":3304,"1497":3305,"1498":3306,"1499":3307,"1500":3308,"1501":3309,"1502":3310,"1503":3311,"1504":3312,"1505":3313,"1506":3314,"1507":3315,"1508":3316,"1509":3317,"1510":3318,"1511":3319,"1512":3320,"1513":3321,"1514":3322,"1548":1452,"1563":1467,"1567":1471,"1569":1473,"1570":1474,"1571":1475,"1572":1476,"1573":1477,"1574":1478,"1575":1479,"1576":1480,"1577":1481,"1578":1482,"1579":1483,"1580":1484,"1581":1485,"1582":1486,"1583":1487,"1584":1488,"1585":1489,"1586":1490,"1587":1491,"1588":1492,"1589":1493,"1590":1494,"1591":1495,"1592":1496,"1593":1497,"1594":1498,"1600":1504,"1601":1505,"1602":1506,"1603":1507,"1604":1508,"1605":1509,"1606":1510,"1607":1511,"1608":1512,"1609":1513,"1610":1514,"1611":1515,"1612":1516,"1613":1517,"1614":1518,"1615":1519,"1616":1520,"1617":1521,"1618":1522,"1619":16778835,"1620":16778836,"1621":16778837,"1632":16778848,"1633":16778849,"1634":16778850,"1635":16778851,"1636":16778852,"1637":16778853,"1638":16778854,"1639":16778855,"1640":16778856,"1641":16778857,"1642":16778858,"1648":16778864,"1657":16778873,"1662":16778878,"1670":16778886,"1672":16778888,"1681":16778897,"1688":16778904,"1700":16778916,"1705":16778921,"1711":16778927,"1722":16778938,"1726":16778942,"1729":16778945,"1740":16778956,"1746":16778962,"1748":16778964,"1776":16778992,"1777":16778993,"1778":16778994,"1779":16778995,"1780":16778996,"1781":16778997,"1782":16778998,"1783":16778999,"1784":16779000,"1785":16779001,"3458":16780674,"3459":16780675,"3461":16780677,"3462":16780678,"3463":16780679,"3464":16780680,"3465":16780681,"3466":16780682,"3467":16780683,"3468":16780684,"3469":16780685,"3470":16780686,"3471":16780687,"3472":16780688,"3473":16780689,"3474":16780690,"3475":16780691,"3476":16780692,"3477":16780693,"3478":16780694,"3482":16780698,"3483":16780699,"3484":16780700,"3485":16780701,"3486":16780702,"3487":16780703,"3488":16780704,"3489":16780705,"3490":16780706,"3491":16780707,"3492":16780708,"3493":16780709,"3494":16780710,"3495":16780711,"3496":16780712,"3497":16780713,"3498":16780714,"3499":16780715,"3500":16780716,"3501":16780717,"3502":16780718,"3503":16780719,"3504":16780720,"3505":16780721,"3507":16780723,"3508":16780724,"3509":16780725,"3510":16780726,"3511":16780727,"3512":16780728,"3513":16780729,"3514":16780730,"3515":16780731,"3517":16780733,"3520":16780736,"3521":16780737,"3522":16780738,"3523":16780739,"3524":16780740,"3525":16780741,"3526":16780742,"3530":16780746,"3535":16780751,"3536":16780752,"3537":16780753,"3538":16780754,"3539":16780755,"3540":16780756,"3542":16780758,"3544":16780760,"3545":16780761,"3546":16780762,"3547":16780763,"3548":16780764,"3549":16780765,"3550":16780766,"3551":16780767,"3570":16780786,"3571":16780787,"3572":16780788,"3585":3489,"3586":3490,"3587":3491,"3588":3492,"3589":3493,"3590":3494,"3591":3495,"3592":3496,"3593":3497,"3594":3498,"3595":3499,"3596":3500,"3597":3501,"3598":3502,"3599":3503,"3600":3504,"3601":3505,"3602":3506,"3603":3507,"3604":3508,"3605":3509,"3606":3510,"3607":3511,"3608":3512,"3609":3513,"3610":3514,"3611":3515,"3612":3516,"3613":3517,"3614":3518,"3615":3519,"3616":3520,"3617":3521,"3618":3522,"3619":3523,"3620":3524,"3621":3525,"3622":3526,"3623":3527,"3624":3528,"3625":3529,"3626":3530,"3627":3531,"3628":3532,"3629":3533,"3630":3534,"3631":3535,"3632":3536,"3633":3537,"3634":3538,"3635":3539,"3636":3540,"3637":3541,"3638":3542,"3639":3543,"3640":3544,"3641":3545,"3642":3546,"3647":3551,"3648":3552,"3649":3553,"3650":3554,"3651":3555,"3652":3556,"3653":3557,"3654":3558,"3655":3559,"3656":3560,"3657":3561,"3658":3562,"3659":3563,"3660":3564,"3661":3565,"3664":3568,"3665":3569,"3666":3570,"3667":3571,"3668":3572,"3669":3573,"3670":3574,"3671":3575,"3672":3576,"3673":3577,"4304":16781520,"4305":16781521,"4306":16781522,"4307":16781523,"4308":16781524,"4309":16781525,"4310":16781526,"4311":16781527,"4312":16781528,"4313":16781529,"4314":16781530,"4315":16781531,"4316":16781532,"4317":16781533,"4318":16781534,"4319":16781535,"4320":16781536,"4321":16781537,"4322":16781538,"4323":16781539,"4324":16781540,"4325":16781541,"4326":16781542,"4327":16781543,"4328":16781544,"4329":16781545,"4330":16781546,"4331":16781547,"4332":16781548,"4333":16781549,"4334":16781550,"4335":16781551,"4336":16781552,"4337":16781553,"4338":16781554,"4339":16781555,"4340":16781556,"4341":16781557,"4342":16781558,"7682":16784898,"7683":16784899,"7690":16784906,"7691":16784907,"7710":16784926,"7711":16784927,"7734":16784950,"7735":16784951,"7744":16784960,"7745":16784961,"7766":16784982,"7767":16784983,"7776":16784992,"7777":16784993,"7786":16785002,"7787":16785003,"7808":16785024,"7809":16785025,"7810":16785026,"7811":16785027,"7812":16785028,"7813":16785029,"7818":16785034,"7819":16785035,"7840":16785056,"7841":16785057,"7842":16785058,"7843":16785059,"7844":16785060,"7845":16785061,"7846":16785062,"7847":16785063,"7848":16785064,"7849":16785065,"7850":16785066,"7851":16785067,"7852":16785068,"7853":16785069,"7854":16785070,"7855":16785071,"7856":16785072,"7857":16785073,"7858":16785074,"7859":16785075,"7860":16785076,"7861":16785077,"7862":16785078,"7863":16785079,"7864":16785080,"7865":16785081,"7866":16785082,"7867":16785083,"7868":16785084,"7869":16785085,"7870":16785086,"7871":16785087,"7872":16785088,"7873":16785089,"7874":16785090,"7875":16785091,"7876":16785092,"7877":16785093,"7878":16785094,"7879":16785095,"7880":16785096,"7881":16785097,"7882":16785098,"7883":16785099,"7884":16785100,"7885":16785101,"7886":16785102,"7887":16785103,"7888":16785104,"7889":16785105,"7890":16785106,"7891":16785107,"7892":16785108,"7893":16785109,"7894":16785110,"7895":16785111,"7896":16785112,"7897":16785113,"7898":16785114,"7899":16785115,"7900":16785116,"7901":16785117,"7902":16785118,"7903":16785119,"7904":16785120,"7905":16785121,"7906":16785122,"7907":16785123,"7908":16785124,"7909":16785125,"7910":16785126,"7911":16785127,"7912":16785128,"7913":16785129,"7914":16785130,"7915":16785131,"7916":16785132,"7917":16785133,"7918":16785134,"7919":16785135,"7920":16785136,"7921":16785137,"7922":16785138,"7923":16785139,"7924":16785140,"7925":16785141,"7926":16785142,"7927":16785143,"7928":16785144,"7929":16785145,"8194":2722,"8195":2721,"8196":2723,"8197":2724,"8199":2725,"8200":2726,"8201":2727,"8202":2728,"8210":2747,"8211":2730,"8212":2729,"8213":1967,"8215":3295,"8216":2768,"8217":2769,"8218":2813,"8220":2770,"8221":2771,"8222":2814,"8224":2801,"8225":2802,"8226":2790,"8229":2735,"8230":2734,"8240":2773,"8242":2774,"8243":2775,"8248":2812,"8254":1150,"8304":16785520,"8308":16785524,"8309":16785525,"8310":16785526,"8311":16785527,"8312":16785528,"8313":16785529,"8320":16785536,"8321":16785537,"8322":16785538,"8323":16785539,"8324":16785540,"8325":16785541,"8326":16785542,"8327":16785543,"8328":16785544,"8329":16785545,"8352":16785568,"8353":16785569,"8354":16785570,"8355":16785571,"8356":16785572,"8357":16785573,"8358":16785574,"8359":16785575,"8360":16785576,"8361":3839,"8362":16785578,"8363":16785579,"8364":8364,"8453":2744,"8470":1712,"8471":2811,"8478":2772,"8482":2761,"8531":2736,"8532":2737,"8533":2738,"8534":2739,"8535":2740,"8536":2741,"8537":2742,"8538":2743,"8539":2755,"8540":2756,"8541":2757,"8542":2758,"8592":2299,"8593":2300,"8594":2301,"8595":2302,"8658":2254,"8660":2253,"8706":2287,"8709":16785925,"8711":2245,"8712":16785928,"8713":16785929,"8715":16785931,"8728":3018,"8730":2262,"8731":16785947,"8732":16785948,"8733":2241,"8734":2242,"8743":2270,"8744":2271,"8745":2268,"8746":2269,"8747":2239,"8748":16785964,"8749":16785965,"8756":2240,"8757":16785973,"8764":2248,"8771":2249,"8773":16785992,"8775":16785991,"8800":2237,"8801":2255,"8802":16786018,"8803":16786019,"8804":2236,"8805":2238,"8834":2266,"8835":2267,"8866":3068,"8867":3036,"8868":3010,"8869":3022,"8968":3027,"8970":3012,"8981":2810,"8992":2212,"8993":2213,"9109":3020,"9115":2219,"9117":2220,"9118":2221,"9120":2222,"9121":2215,"9123":2216,"9124":2217,"9126":2218,"9128":2223,"9132":2224,"9143":2209,"9146":2543,"9147":2544,"9148":2546,"9149":2547,"9225":2530,"9226":2533,"9227":2537,"9228":2531,"9229":2532,"9251":2732,"9252":2536,"9472":2211,"9474":2214,"9484":2210,"9488":2539,"9492":2541,"9496":2538,"9500":2548,"9508":2549,"9516":2551,"9524":2550,"9532":2542,"9618":2529,"9642":2791,"9643":2785,"9644":2779,"9645":2786,"9646":2783,"9647":2767,"9650":2792,"9651":2787,"9654":2781,"9655":2765,"9660":2793,"9661":2788,"9664":2780,"9665":2764,"9670":2528,"9675":2766,"9679":2782,"9702":2784,"9734":2789,"9742":2809,"9747":2762,"9756":2794,"9758":2795,"9792":2808,"9794":2807,"9827":2796,"9829":2798,"9830":2797,"9837":2806,"9839":2805,"10003":2803,"10007":2804,"10013":2777,"10016":2800,"10216":2748,"10217":2750,"10240":16787456,"10241":16787457,"10242":16787458,"10243":16787459,"10244":16787460,"10245":16787461,"10246":16787462,"10247":16787463,"10248":16787464,"10249":16787465,"10250":16787466,"10251":16787467,"10252":16787468,"10253":16787469,"10254":16787470,"10255":16787471,"10256":16787472,"10257":16787473,"10258":16787474,"10259":16787475,"10260":16787476,"10261":16787477,"10262":16787478,"10263":16787479,"10264":16787480,"10265":16787481,"10266":16787482,"10267":16787483,"10268":16787484,"10269":16787485,"10270":16787486,"10271":16787487,"10272":16787488,"10273":16787489,"10274":16787490,"10275":16787491,"10276":16787492,"10277":16787493,"10278":16787494,"10279":16787495,"10280":16787496,"10281":16787497,"10282":16787498,"10283":16787499,"10284":16787500,"10285":16787501,"10286":16787502,"10287":16787503,"10288":16787504,"10289":16787505,"10290":16787506,"10291":16787507,"10292":16787508,"10293":16787509,"10294":16787510,"10295":16787511,"10296":16787512,"10297":16787513,"10298":16787514,"10299":16787515,"10300":16787516,"10301":16787517,"10302":16787518,"10303":16787519,"10304":16787520,"10305":16787521,"10306":16787522,"10307":16787523,"10308":16787524,"10309":16787525,"10310":16787526,"10311":16787527,"10312":16787528,"10313":16787529,"10314":16787530,"10315":16787531,"10316":16787532,"10317":16787533,"10318":16787534,"10319":16787535,"10320":16787536,"10321":16787537,"10322":16787538,"10323":16787539,"10324":16787540,"10325":16787541,"10326":16787542,"10327":16787543,"10328":16787544,"10329":16787545,"10330":16787546,"10331":16787547,"10332":16787548,"10333":16787549,"10334":16787550,"10335":16787551,"10336":16787552,"10337":16787553,"10338":16787554,"10339":16787555,"10340":16787556,"10341":16787557,"10342":16787558,"10343":16787559,"10344":16787560,"10345":16787561,"10346":16787562,"10347":16787563,"10348":16787564,"10349":16787565,"10350":16787566,"10351":16787567,"10352":16787568,"10353":16787569,"10354":16787570,"10355":16787571,"10356":16787572,"10357":16787573,"10358":16787574,"10359":16787575,"10360":16787576,"10361":16787577,"10362":16787578,"10363":16787579,"10364":16787580,"10365":16787581,"10366":16787582,"10367":16787583,"10368":16787584,"10369":16787585,"10370":16787586,"10371":16787587,"10372":16787588,"10373":16787589,"10374":16787590,"10375":16787591,"10376":16787592,"10377":16787593,"10378":16787594,"10379":16787595,"10380":16787596,"10381":16787597,"10382":16787598,"10383":16787599,"10384":16787600,"10385":16787601,"10386":16787602,"10387":16787603,"10388":16787604,"10389":16787605,"10390":16787606,"10391":16787607,"10392":16787608,"10393":16787609,"10394":16787610,"10395":16787611,"10396":16787612,"10397":16787613,"10398":16787614,"10399":16787615,"10400":16787616,"10401":16787617,"10402":16787618,"10403":16787619,"10404":16787620,"10405":16787621,"10406":16787622,"10407":16787623,"10408":16787624,"10409":16787625,"10410":16787626,"10411":16787627,"10412":16787628,"10413":16787629,"10414":16787630,"10415":16787631,"10416":16787632,"10417":16787633,"10418":16787634,"10419":16787635,"10420":16787636,"10421":16787637,"10422":16787638,"10423":16787639,"10424":16787640,"10425":16787641,"10426":16787642,"10427":16787643,"10428":16787644,"10429":16787645,"10430":16787646,"10431":16787647,"10432":16787648,"10433":16787649,"10434":16787650,"10435":16787651,"10436":16787652,"10437":16787653,"10438":16787654,"10439":16787655,"10440":16787656,"10441":16787657,"10442":16787658,"10443":16787659,"10444":16787660,"10445":16787661,"10446":16787662,"10447":16787663,"10448":16787664,"10449":16787665,"10450":16787666,"10451":16787667,"10452":16787668,"10453":16787669,"10454":16787670,"10455":16787671,"10456":16787672,"10457":16787673,"10458":16787674,"10459":16787675,"10460":16787676,"10461":16787677,"10462":16787678,"10463":16787679,"10464":16787680,"10465":16787681,"10466":16787682,"10467":16787683,"10468":16787684,"10469":16787685,"10470":16787686,"10471":16787687,"10472":16787688,"10473":16787689,"10474":16787690,"10475":16787691,"10476":16787692,"10477":16787693,"10478":16787694,"10479":16787695,"10480":16787696,"10481":16787697,"10482":16787698,"10483":16787699,"10484":16787700,"10485":16787701,"10486":16787702,"10487":16787703,"10488":16787704,"10489":16787705,"10490":16787706,"10491":16787707,"10492":16787708,"10493":16787709,"10494":16787710,"10495":16787711,"12289":1188,"12290":1185,"12300":1186,"12301":1187,"12443":1246,"12444":1247,"12449":1191,"12450":1201,"12451":1192,"12452":1202,"12453":1193,"12454":1203,"12455":1194,"12456":1204,"12457":1195,"12458":1205,"12459":1206,"12461":1207,"12463":1208,"12465":1209,"12467":1210,"12469":1211,"12471":1212,"12473":1213,"12475":1214,"12477":1215,"12479":1216,"12481":1217,"12483":1199,"12484":1218,"12486":1219,"12488":1220,"12490":1221,"12491":1222,"12492":1223,"12493":1224,"12494":1225,"12495":1226,"12498":1227,"12501":1228,"12504":1229,"12507":1230,"12510":1231,"12511":1232,"12512":1233,"12513":1234,"12514":1235,"12515":1196,"12516":1236,"12517":1197,"12518":1237,"12519":1198,"12520":1238,"12521":1239,"12522":1240,"12523":1241,"12524":1242,"12525":1243,"12527":1244,"12530":1190,"12531":1245,"12539":1189,"12540":1200}; + + function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; } + return { + fromUnicode : function(u) { return lookup(codepoints[u]); }, + lookup : lookup + }; +})(); diff --git a/webclients/novnc/include/playback.js b/webclients/novnc/include/playback.js index a21c7b6..7756529 100644 --- a/webclients/novnc/include/playback.js +++ b/webclients/novnc/include/playback.js @@ -1,7 +1,7 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin - * Licensed under LGPL-3 (see LICENSE.LGPL-3) + * Licensed under MPL 2.0 (see LICENSE.txt) */ "use strict"; @@ -79,10 +79,22 @@ queue_next_packet = function () { } }; +var bytes_processed = 0; + do_packet = function () { //Util.Debug("Processing frame: " + frame_idx); - var frame = VNC_frame_data[frame_idx]; - rfb.recv_message({'data' : frame.slice(frame.indexOf('{', 1) + 1)}); + var frame = VNC_frame_data[frame_idx], + start = frame.indexOf('{', 1) + 1; + bytes_processed += frame.length - start; + if (VNC_frame_encoding === 'binary') { + var u8 = new Uint8Array(frame.length - start); + for (var i = 0; i < frame.length - start; i++) { + u8[i] = frame.charCodeAt(start + i); + } + rfb.recv_message({'data' : u8}); + } else { + rfb.recv_message({'data' : frame.slice(start)}); + } frame_idx += 1; queue_next_packet(); diff --git a/webclients/novnc/include/rfb.js b/webclients/novnc/include/rfb.js index 00fb7d8..0afe656 100644 --- a/webclients/novnc/include/rfb.js +++ b/webclients/novnc/include/rfb.js @@ -1,7 +1,8 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin - * Licensed under LGPL-3 (see LICENSE.txt) + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. * @@ -9,1841 +10,1874 @@ * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) */ -/*jslint white: false, browser: true, bitwise: false, plusplus: false */ +/*jslint white: false, browser: true */ /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ +var RFB; -function RFB(defaults) { -"use strict"; - -var that = {}, // Public API methods - conf = {}, // Configuration attributes - - // Pre-declare private functions used before definitions (jslint) - init_vars, updateState, fail, handle_message, - init_msg, normal_msg, framebufferUpdate, print_stats, - - pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests, - keyEvent, pointerEvent, clientCutText, - - getTightCLength, extract_data_uri, - keyPress, mouseButton, mouseMove, - - checkEvents, // Overridable for testing - - - // - // Private RFB namespace variables - // - rfb_host = '', - rfb_port = 5900, - rfb_password = '', - rfb_path = '', - - rfb_state = 'disconnected', - rfb_version = 0, - rfb_max_version= 3.8, - rfb_auth_scheme= '', - - - // In preference order - encodings = [ - ['COPYRECT', 0x01 ], - ['TIGHT', 0x07 ], - ['TIGHT_PNG', -260 ], - ['HEXTILE', 0x05 ], - ['RRE', 0x02 ], - ['RAW', 0x00 ], - ['DesktopSize', -223 ], - ['Cursor', -239 ], - - // Psuedo-encoding settings - //['JPEG_quality_lo', -32 ], - ['JPEG_quality_med', -26 ], - //['JPEG_quality_hi', -23 ], - //['compress_lo', -255 ], - ['compress_hi', -247 ], - ['last_rect', -224 ] - ], - - encHandlers = {}, - encNames = {}, - encStats = {}, // [rectCnt, rectCntTot] - - ws = null, // Websock object - display = null, // Display object - keyboard = null, // Keyboard input handler object - mouse = null, // Mouse input handler object - sendTimer = null, // Send Queue check timer - connTimer = null, // connection timer - disconnTimer = null, // disconnection timer - msgTimer = null, // queued handle_message timer - - // Frame buffer update state - FBU = { - rects : 0, - subrects : 0, // RRE - lines : 0, // RAW - tiles : 0, // HEXTILE - bytes : 0, - x : 0, - y : 0, - width : 0, - height : 0, - encoding : 0, - subencoding : -1, - background : null, - zlibs : [] // TIGHT zlib streams - }, - - fb_Bpp = 4, - fb_depth = 3, - fb_width = 0, - fb_height = 0, - fb_name = "", - - last_req_time = 0, - rre_chunk_sz = 100, - - timing = { - last_fbu : 0, - fbu_total : 0, - fbu_total_cnt : 0, - full_fbu_total : 0, - full_fbu_cnt : 0, - - fbu_rt_start : 0, - fbu_rt_total : 0, - fbu_rt_cnt : 0, - pixels : 0 - }, - - test_mode = false, - - def_con_timeout = Websock_native ? 2 : 5, - - /* Mouse state */ - mouse_buttonMask = 0, - mouse_arr = [], - viewportDragging = false, - viewportDragPos = {}; - -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'wo', 'dom', null, 'VNC display rendering Canvas object'], - ['focusContainer', 'wo', 'dom', document, 'DOM element that captures keyboard input'], - - ['encrypt', 'rw', 'bool', false, 'Use TLS/SSL/wss encryption'], - ['true_color', 'rw', 'bool', true, 'Request true color pixel data'], - ['local_cursor', 'rw', 'bool', false, 'Request locally rendered cursor'], - ['shared', 'rw', 'bool', true, 'Request shared mode'], - ['view_only', 'rw', 'bool', false, 'Disable client mouse/keyboard'], - - ['connectTimeout', 'rw', 'int', def_con_timeout, 'Time (s) to wait for connection'], - ['disconnectTimeout', 'rw', 'int', 3, 'Time (s) to wait for disconnection'], - - // UltraVNC repeater ID to connect to - ['repeaterID', 'rw', 'str', '', 'RepeaterID to connect to'], - - ['viewportDrag', 'rw', 'bool', false, 'Move the viewport on mouse drags'], - - ['check_rate', 'rw', 'int', 217, 'Timing (ms) of send/receive check'], - ['fbu_req_rate', 'rw', 'int', 1413, 'Timing (ms) of frameBufferUpdate requests'], - - // Callback functions - ['onUpdateState', 'rw', 'func', function() { }, - 'onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change '], - ['onPasswordRequired', 'rw', 'func', function() { }, - 'onPasswordRequired(rfb): VNC password is required '], - ['onClipboard', 'rw', 'func', function() { }, - 'onClipboard(rfb, text): RFB clipboard contents received'], - ['onBell', 'rw', 'func', function() { }, - 'onBell(rfb): RFB Bell message received '], - ['onFBUReceive', 'rw', 'func', function() { }, - 'onFBUReceive(rfb, fbu): RFB FBU received but not yet processed '], - ['onFBUComplete', 'rw', 'func', function() { }, - 'onFBUComplete(rfb, fbu): RFB FBU received and processed '], - - // These callback names are deprecated - ['updateState', 'rw', 'func', function() { }, - 'obsolete, use onUpdateState'], - ['clipboardReceive', 'rw', 'func', function() { }, - 'obsolete, use onClipboard'] - ]); +(function () { + "use strict"; + RFB = function (defaults) { + if (!defaults) { + defaults = {}; + } + this._rfb_host = ''; + this._rfb_port = 5900; + this._rfb_password = ''; + this._rfb_path = ''; + + this._rfb_state = 'disconnected'; + this._rfb_version = 0; + this._rfb_max_version = 3.8; + this._rfb_auth_scheme = ''; + + this._rfb_tightvnc = false; + this._rfb_xvp_ver = 0; + + // In preference order + this._encodings = [ + ['COPYRECT', 0x01 ], + ['TIGHT', 0x07 ], + ['TIGHT_PNG', -260 ], + ['HEXTILE', 0x05 ], + ['RRE', 0x02 ], + ['RAW', 0x00 ], + ['DesktopSize', -223 ], + ['Cursor', -239 ], + + // Psuedo-encoding settings + //['JPEG_quality_lo', -32 ], + ['JPEG_quality_med', -26 ], + //['JPEG_quality_hi', -23 ], + //['compress_lo', -255 ], + ['compress_hi', -247 ], + ['last_rect', -224 ], + ['xvp', -309 ] + ]; + + this._encHandlers = {}; + this._encNames = {}; + this._encStats = {}; + + this._sock = null; // Websock object + this._display = null; // Display object + this._keyboard = null; // Keyboard input handler object + this._mouse = null; // Mouse input handler object + this._sendTimer = null; // Send Queue check timer + this._disconnTimer = null; // disconnection timer + this._msgTimer = null; // queued handle_msg timer + + // Frame buffer update state + this._FBU = { + rects: 0, + subrects: 0, // RRE + lines: 0, // RAW + tiles: 0, // HEXTILE + bytes: 0, + x: 0, + y: 0, + width: 0, + height: 0, + encoding: 0, + subencoding: -1, + background: null, + zlib: [] // TIGHT zlib streams + }; -// Override/add some specific configuration getters/setters -that.set_local_cursor = function(cursor) { - if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) { - conf.local_cursor = false; - } else { - if (display.get_cursor_uri()) { - conf.local_cursor = true; - } else { - Util.Warn("Browser does not support local cursor"); - } - } -}; - -// These are fake configuration getters -that.get_display = function() { return display; }; - -that.get_keyboard = function() { return keyboard; }; - -that.get_mouse = function() { return mouse; }; - - - -// -// Setup routines -// - -// Create the public API interface and initialize values that stay -// constant across connect/disconnect -function constructor() { - var i, rmode; - Util.Debug(">> RFB.constructor"); - - // Create lookup tables based encoding number - for (i=0; i < encodings.length; i+=1) { - encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]]; - encNames[encodings[i][1]] = encodings[i][0]; - encStats[encodings[i][1]] = [0, 0]; - } - // Initialize display, mouse, keyboard, and websock - try { - display = new Display({'target': conf.target}); - } catch (exc) { - Util.Error("Display exception: " + exc); - updateState('fatal', "No working Display"); - } - keyboard = new Keyboard({'target': conf.focusContainer, - 'onKeyPress': keyPress}); - mouse = new Mouse({'target': conf.target, - 'onMouseButton': mouseButton, - 'onMouseMove': mouseMove}); - - rmode = display.get_render_mode(); - - ws = new Websock(); - ws.on('message', handle_message); - ws.on('open', function() { - if (rfb_state === "connect") { - updateState('ProtocolVersion', "Starting VNC handshake"); - } else { - fail("Got unexpected WebSockets connection"); - } - }); - ws.on('close', function(e) { - Util.Warn("WebSocket on-close event"); - var msg = ""; - if (e.code) { - msg = " (code: " + e.code; - if (e.reason) { - msg += ", reason: " + e.reason; - } - msg += ")"; - } - if (rfb_state === 'disconnect') { - updateState('disconnected', 'VNC disconnected' + msg); - } else if (rfb_state === 'ProtocolVersion') { - fail('Failed to connect to server' + msg); - } else if (rfb_state in {'failed':1, 'disconnected':1}) { - Util.Error("Received onclose while disconnected" + msg); - } else { - fail('Server disconnected' + msg); + this._fb_Bpp = 4; + this._fb_depth = 3; + this._fb_width = 0; + this._fb_height = 0; + this._fb_name = ""; + + this._rre_chunk_sz = 100; + + this._timing = { + last_fbu: 0, + fbu_total: 0, + fbu_total_cnt: 0, + full_fbu_total: 0, + full_fbu_cnt: 0, + + fbu_rt_start: 0, + fbu_rt_total: 0, + fbu_rt_cnt: 0, + pixels: 0 + }; + + // Mouse state + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._viewportDragging = false; + this._viewportDragPos = {}; + + // set the default value on user-facing properties + Util.set_defaults(this, defaults, { + 'target': 'null', // VNC display rendering Canvas object + 'focusContainer': document, // DOM element that captures keyboard input + 'encrypt': false, // Use TLS/SSL/wss encryption + 'true_color': true, // Request true color pixel data + 'local_cursor': false, // Request locally rendered cursor + 'shared': true, // Request shared mode + 'view_only': false, // Disable client mouse/keyboard + 'xvp_password_sep': '@', // Separator for XVP password fields + 'disconnectTimeout': 3, // Time (s) to wait for disconnection + 'wsProtocols': ['binary', 'base64'], // Protocols to use in the WebSocket connection + 'repeaterID': '', // [UltraVNC] RepeaterID to connect to + 'viewportDrag': false, // Move the viewport on mouse drags + + // Callback functions + 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change + 'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required + 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received + 'onBell': function () { }, // onBell(rfb): RFB Bell message received + 'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed + 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized + 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received + 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection + }); + + // main setup + Util.Debug(">> RFB.constructor"); + + // populate encHandlers with bound versions + Object.keys(RFB.encodingHandlers).forEach(function (encName) { + this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this); + }.bind(this)); + + // Create lookup tables based on encoding number + for (var i = 0; i < this._encodings.length; i++) { + this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]]; + this._encNames[this._encodings[i][1]] = this._encodings[i][0]; + this._encStats[this._encodings[i][1]] = [0, 0]; } - }); - ws.on('error', function(e) { - Util.Warn("WebSocket on-error event"); - //fail("WebSock reported an error"); - }); - - - init_vars(); - - /* Check web-socket-js if no builtin WebSocket support */ - if (Websock_native) { - Util.Info("Using native WebSockets"); - updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); - } else { - Util.Warn("Using web-socket-js bridge. Flash version: " + - Util.Flash.version); - if ((! Util.Flash) || - (Util.Flash.version < 9)) { - updateState('fatal', "WebSockets or Adobe Flash<\/a> is required"); - } else if (document.location.href.substr(0, 7) === "file://") { - updateState('fatal', - "'file://' URL is incompatible with Adobe Flash"); - } else { - updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); + + try { + this._display = new Display({target: this._target}); + } catch (exc) { + Util.Error("Display exception: " + exc); + this._updateState('fatal', "No working Display"); } - } - - Util.Debug("<< RFB.constructor"); - return that; // Return the public API interface -} - -function connect() { - Util.Debug(">> RFB.connect"); - var uri; - - if (typeof UsingSocketIO !== "undefined") { - uri = "http://" + rfb_host + ":" + rfb_port + "/" + rfb_path; - } else { - if (conf.encrypt) { - uri = "wss://"; + + this._keyboard = new Keyboard({target: this._focusContainer, + onKeyPress: this._handleKeyPress.bind(this)}); + + this._mouse = new Mouse({target: this._target, + onMouseButton: this._handleMouseButton.bind(this), + onMouseMove: this._handleMouseMove.bind(this), + notify: this._keyboard.sync.bind(this._keyboard)}); + + this._sock = new Websock(); + this._sock.on('message', this._handle_message.bind(this)); + this._sock.on('open', function () { + if (this._rfb_state === 'connect') { + this._updateState('ProtocolVersion', "Starting VNC handshake"); + } else { + this._fail("Got unexpected WebSocket connection"); + } + }.bind(this)); + this._sock.on('close', function (e) { + Util.Warn("WebSocket on-close event"); + var msg = ""; + if (e.code) { + msg = " (code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + if (this._rfb_state === 'disconnect') { + this._updateState('disconnected', 'VNC disconnected' + msg); + } else if (this._rfb_state === 'ProtocolVersion') { + this._fail('Failed to connect to server' + msg); + } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) { + Util.Error("Received onclose while disconnected" + msg); + } else { + this._fail("Server disconnected" + msg); + } + }.bind(this)); + this._sock.on('error', function (e) { + Util.Warn("WebSocket on-error event"); + }); + + this._init_vars(); + + var rmode = this._display.get_render_mode(); + if (Websock_native) { + Util.Info("Using native WebSockets"); + this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); } else { - uri = "ws://"; - } - uri += rfb_host + ":" + rfb_port + "/" + rfb_path; - } - Util.Info("connecting to " + uri); - ws.open(uri); - - Util.Debug("<< RFB.connect"); -} - -// Initialize variables that are reset before each connection -init_vars = function() { - var i; - - /* Reset state */ - ws.init(); - - FBU.rects = 0; - FBU.subrects = 0; // RRE and HEXTILE - FBU.lines = 0; // RAW - FBU.tiles = 0; // HEXTILE - FBU.zlibs = []; // TIGHT zlib encoders - mouse_buttonMask = 0; - mouse_arr = []; - - // Clear the per connection encoding stats - for (i=0; i < encodings.length; i+=1) { - encStats[encodings[i][1]][0] = 0; - } - - for (i=0; i < 4; i++) { - //FBU.zlibs[i] = new InflateStream(); - FBU.zlibs[i] = new TINF(); - FBU.zlibs[i].init(); - } -}; - -// Print statistics -print_stats = function() { - var i, s; - Util.Info("Encoding stats for this connection:"); - for (i=0; i < encodings.length; i+=1) { - s = encStats[encodings[i][1]]; - if ((s[0] + s[1]) > 0) { - Util.Info(" " + encodings[i][0] + ": " + - s[0] + " rects"); - } - } - Util.Info("Encoding stats since page load:"); - for (i=0; i < encodings.length; i+=1) { - s = encStats[encodings[i][1]]; - if ((s[0] + s[1]) > 0) { - Util.Info(" " + encodings[i][0] + ": " + - s[1] + " rects"); + Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version); + if (!Util.Flash || Util.Flash.version < 9) { + this._updateState('fatal', "WebSockets or Adobe Flash is required"); + } else if (document.location.href.substr(0, 7) === 'file://') { + this._updateState('fatal', "'file://' URL is incompatible with Adobe Flash"); + } else { + this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); + } } - } -}; -// -// Utility routines -// + Util.Debug("<< RFB.constructor"); + }; + RFB.prototype = { + // Public methods + connect: function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; -/* - * Page states: - * loaded - page load, equivalent to disconnected - * disconnected - idle state - * connect - starting to connect (to ProtocolVersion) - * normal - connected - * disconnect - starting to disconnect - * failed - abnormal disconnect - * fatal - failed to load page, or fatal error - * - * RFB protocol initialization states: - * ProtocolVersion - * Security - * Authentication - * password - waiting for password, not part of RFB - * SecurityResult - * ClientInitialization - not triggered by server message - * ServerInitialization (to normal) - */ -updateState = function(state, statusMsg) { - var func, cmsg, oldstate = rfb_state; - - if (state === oldstate) { - /* Already here, ignore */ - Util.Debug("Already in state '" + state + "', ignoring."); - return; - } - - /* - * These are disconnected states. A previous connect may - * asynchronously cause a connection so make sure we are closed. - */ - if (state in {'disconnected':1, 'loaded':1, 'connect':1, - 'disconnect':1, 'failed':1, 'fatal':1}) { - if (sendTimer) { - clearInterval(sendTimer); - sendTimer = null; - } + if (!this._rfb_host || !this._rfb_port) { + return this._fail("Must set host and port"); + } - if (msgTimer) { - clearInterval(msgTimer); - msgTimer = null; - } + this._updateState('connect'); + }, + + disconnect: function () { + this._updateState('disconnect', 'Disconnecting'); + }, + + sendPassword: function (passwd) { + this._rfb_password = passwd; + this._rfb_state = 'Authentication'; + setTimeout(this._init_msg.bind(this), 1); + }, + + sendCtrlAltDel: function () { + if (this._rfb_state !== 'normal' || this._view_only) { return false; } + Util.Info("Sending Ctrl-Alt-Del"); + + var arr = []; + arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 1)); // Control + arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 1)); // Alt + arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 1)); // Delete + arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 0)); // Delete + arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 0)); // Alt + arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 0)); // Control + this._sock.send(arr); + }, + + xvpOp: function (ver, op) { + if (this._rfb_xvp_ver < ver) { return false; } + Util.Info("Sending XVP operation " + op + " (version " + ver + ")"); + this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); + return true; + }, + + xvpShutdown: function () { + return this.xvpOp(1, 2); + }, + + xvpReboot: function () { + return this.xvpOp(1, 3); + }, + + xvpReset: function () { + return this.xvpOp(1, 4); + }, + + // Send a key press. If 'down' is not specified then send a down key + // followed by an up key. + sendKey: function (code, down) { + if (this._rfb_state !== "normal" || this._view_only) { return false; } + var arr = []; + if (typeof down !== 'undefined') { + Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); + arr = arr.concat(RFB.messages.keyEvent(code, down ? 1 : 0)); + } else { + Util.Info("Sending key code (down + up): " + code); + arr = arr.concat(RFB.messages.keyEvent(code, 1)); + arr = arr.concat(RFB.messages.keyEvent(code, 0)); + } + this._sock.send(arr); + }, + + clipboardPasteFrom: function (text) { + if (this._rfb_state !== 'normal') { return; } + this._sock.send(RFB.messages.clientCutText(text)); + }, - if (display && display.get_context()) { - keyboard.ungrab(); - mouse.ungrab(); - display.defaultCursor(); - if ((Util.get_logging() !== 'debug') || - (state === 'loaded')) { - // Show noVNC logo on load and when disconnected if - // debug is off - display.clear(); + // Private methods + + _connect: function () { + Util.Debug(">> RFB.connect"); + + var uri; + if (typeof UsingSocketIO !== 'undefined') { + uri = 'http'; + } else { + uri = this._encrypt ? 'wss' : 'ws'; } - } - ws.close(); - } - - if (oldstate === 'fatal') { - Util.Error("Fatal error, cannot continue"); - } - - if ((state === 'failed') || (state === 'fatal')) { - func = Util.Error; - } else { - func = Util.Warn; - } - - cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; - func("New state '" + state + "', was '" + oldstate + "'." + cmsg); - - if ((oldstate === 'failed') && (state === 'disconnected')) { - // Do disconnect action, but stay in failed state - rfb_state = 'failed'; - } else { - rfb_state = state; - } - - if (connTimer && (rfb_state !== 'connect')) { - Util.Debug("Clearing connect timer"); - clearInterval(connTimer); - connTimer = null; - } - - if (disconnTimer && (rfb_state !== 'disconnect')) { - Util.Debug("Clearing disconnect timer"); - clearInterval(disconnTimer); - disconnTimer = null; - } - - switch (state) { - case 'normal': - if ((oldstate === 'disconnected') || (oldstate === 'failed')) { - Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); - } + uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; + Util.Info("connecting to " + uri); - break; + this._sock.open(uri, this._wsProtocols); + Util.Debug("<< RFB.connect"); + }, - case 'connect': - - connTimer = setTimeout(function () { - fail("Connect timeout"); - }, conf.connectTimeout * 1000); + _init_vars: function () { + // reset state + this._sock.init(); - init_vars(); - connect(); + this._FBU.rects = 0; + this._FBU.subrects = 0; // RRE and HEXTILE + this._FBU.lines = 0; // RAW + this._FBU.tiles = 0; // HEXTILE + this._FBU.zlibs = []; // TIGHT zlib encoders + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._rfb_tightvnc = false; - // WebSocket.onopen transitions to 'ProtocolVersion' - break; + // Clear the per connection encoding stats + var i; + for (i = 0; i < this._encodings.length; i++) { + this._encStats[this._encodings[i][1]][0] = 0; + } + + for (i = 0; i < 4; i++) { + this._FBU.zlibs[i] = new TINF(); + this._FBU.zlibs[i].init(); + } + }, + + _print_stats: function () { + Util.Info("Encoding stats for this connection:"); + var i, s; + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + if (s[0] + s[1] > 0) { + Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects"); + } + } + Util.Info("Encoding stats since page load:"); + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects"); + } + }, - case 'disconnect': - if (! test_mode) { - disconnTimer = setTimeout(function () { - fail("Disconnect timeout"); - }, conf.disconnectTimeout * 1000); - } + /* + * Page states: + * loaded - page load, equivalent to disconnected + * disconnected - idle state + * connect - starting to connect (to ProtocolVersion) + * normal - connected + * disconnect - starting to disconnect + * failed - abnormal disconnect + * fatal - failed to load page, or fatal error + * + * RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * password - waiting for password, not part of RFB + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization (to normal) + */ + _updateState: function (state, statusMsg) { + var oldstate = this._rfb_state; + + if (state === oldstate) { + // Already here, ignore + Util.Debug("Already in state '" + state + "', ignoring"); + } - print_stats(); + /* + * These are disconnected states. A previous connect may + * asynchronously cause a connection so make sure we are closed. + */ + if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1, + 'disconnect': 1, 'failed': 1, 'fatal': 1}) { - // WebSocket.onclose transitions to 'disconnected' - break; + if (this._sendTimer) { + clearInterval(this._sendTimer); + this._sendTimer = null; + } + if (this._msgTimer) { + clearInterval(this._msgTimer); + this._msgTimer = null; + } - case 'failed': - if (oldstate === 'disconnected') { - Util.Error("Invalid transition from 'disconnected' to 'failed'"); - } - if (oldstate === 'normal') { - Util.Error("Error while connected."); - } - if (oldstate === 'init') { - Util.Error("Error while initializing."); - } + if (this._display && this._display.get_context()) { + this._keyboard.ungrab(); + this._mouse.ungrab(); + this._display.defaultCursor(); + if (Util.get_logging() !== 'debug' || state === 'loaded') { + // Show noVNC logo on load and when disconnected, unless in + // debug mode + this._display.clear(); + } + } - // Make sure we transition to disconnected - setTimeout(function() { updateState('disconnected'); }, 50); - - break; - - - default: - // No state change action to take - - } - - if ((oldstate === 'failed') && (state === 'disconnected')) { - // Leave the failed message - conf.updateState(that, state, oldstate); // Obsolete - conf.onUpdateState(that, state, oldstate); - } else { - conf.updateState(that, state, oldstate, statusMsg); // Obsolete - conf.onUpdateState(that, state, oldstate, statusMsg); - } -}; - -fail = function(msg) { - updateState('failed', msg); - return false; -}; - -handle_message = function() { - //Util.Debug(">> handle_message ws.rQlen(): " + ws.rQlen()); - //Util.Debug("ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); - if (ws.rQlen() === 0) { - Util.Warn("handle_message called on empty receive queue"); - return; - } - switch (rfb_state) { - case 'disconnected': - case 'failed': - Util.Error("Got data while disconnected"); - break; - case 'normal': - if (normal_msg() && ws.rQlen() > 0) { - // true means we can continue processing - // Give other events a chance to run - if (msgTimer === null) { - Util.Debug("More data to process, creating timer"); - msgTimer = setTimeout(function () { - msgTimer = null; - handle_message(); - }, 10); - } else { - Util.Debug("More data to process, existing timer"); + this._sock.close(); } - } - break; - default: - init_msg(); - break; - } -}; - - -function genDES(password, challenge) { - var i, passwd = []; - for (i=0; i < password.length; i += 1) { - passwd.push(password.charCodeAt(i)); - } - return (new DES(passwd)).encrypt(challenge); -} - -function flushClient() { - if (mouse_arr.length > 0) { - //send(mouse_arr.concat(fbUpdateRequests())); - ws.send(mouse_arr); - setTimeout(function() { - ws.send(fbUpdateRequests()); - }, 50); - - mouse_arr = []; - return true; - } else { - return false; - } -} - -// overridable for testing -checkEvents = function() { - var now; - if (rfb_state === 'normal' && !viewportDragging) { - if (! flushClient()) { - now = new Date().getTime(); - if (now > last_req_time + conf.fbu_req_rate) { - last_req_time = now; - ws.send(fbUpdateRequests()); + + if (oldstate === 'fatal') { + Util.Error('Fatal error, cannot continue'); } - } - } - setTimeout(checkEvents, conf.check_rate); -}; - -keyPress = function(keysym, down) { - var arr; - - if (conf.view_only) { return; } // View only, skip keyboard events - - arr = keyEvent(keysym, down); - arr = arr.concat(fbUpdateRequests()); - ws.send(arr); -}; - -mouseButton = function(x, y, down, bmask) { - if (down) { - mouse_buttonMask |= bmask; - } else { - mouse_buttonMask ^= bmask; - } - - if (conf.viewportDrag) { - if (down && !viewportDragging) { - viewportDragging = true; - viewportDragPos = {'x': x, 'y': y}; - - // Skip sending mouse events - return; - } else { - viewportDragging = false; - ws.send(fbUpdateRequests()); // Force immediate redraw - } - } - if (conf.view_only) { return; } // View only, skip mouse events + var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; + var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg; + if (state === 'failed' || state === 'fatal') { + Util.Error(cmsg); + } else { + Util.Warn(cmsg); + } - mouse_arr = mouse_arr.concat( - pointerEvent(display.absX(x), display.absY(y)) ); - flushClient(); -}; + if (oldstate === 'failed' && state === 'disconnected') { + // do disconnect action, but stay in failed state + this._rfb_state = 'failed'; + } else { + this._rfb_state = state; + } -mouseMove = function(x, y) { - //Util.Debug('>> mouseMove ' + x + "," + y); - var deltaX, deltaY; + if (this._disconnTimer && this._rfb_state !== 'disconnect') { + Util.Debug("Clearing disconnect timer"); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + } - if (viewportDragging) { - //deltaX = x - viewportDragPos.x; // drag viewport - deltaX = viewportDragPos.x - x; // drag frame buffer - //deltaY = y - viewportDragPos.y; // drag viewport - deltaY = viewportDragPos.y - y; // drag frame buffer - viewportDragPos = {'x': x, 'y': y}; + switch (state) { + case 'normal': + if (oldstate === 'disconnected' || oldstate === 'failed') { + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + } + break; + + case 'connect': + this._init_vars(); + this._connect(); + // WebSocket.onopen transitions to 'ProtocolVersion' + break; + + case 'disconnect': + this._disconnTimer = setTimeout(function () { + this._fail("Disconnect timeout"); + }.bind(this), this._disconnectTimeout * 1000); + + this._print_stats(); + + // WebSocket.onclose transitions to 'disconnected' + break; + + case 'failed': + if (oldstate === 'disconnected') { + Util.Error("Invalid transition from 'disconnected' to 'failed'"); + } else if (oldstate === 'normal') { + Util.Error("Error while connected."); + } else if (oldstate === 'init') { + Util.Error("Error while initializing."); + } - display.viewportChange(deltaX, deltaY); + // Make sure we transition to disconnected + setTimeout(function () { + this._updateState('disconnected'); + }.bind(this), 50); - // Skip sending mouse events - return; - } + break; - if (conf.view_only) { return; } // View only, skip mouse events + default: + // No state change action to take + } - mouse_arr = mouse_arr.concat( - pointerEvent(display.absX(x), display.absY(y)) ); -}; + if (oldstate === 'failed' && state === 'disconnected') { + this._onUpdateState(this, state, oldstate); + } else { + this._onUpdateState(this, state, oldstate, statusMsg); + } + }, + _fail: function (msg) { + this._updateState('failed', msg); + return false; + }, -// -// Server message handlers -// + _handle_message: function () { + if (this._sock.rQlen() === 0) { + Util.Warn("handle_message called on an empty receive queue"); + return; + } -// RFB/VNC initialisation message handler -init_msg = function() { - //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']"); + switch (this._rfb_state) { + case 'disconnected': + case 'failed': + Util.Error("Got data while disconnected"); + break; + case 'normal': + if (this._normal_msg() && this._sock.rQlen() > 0) { + // true means we can continue processing + // Give other events a chance to run + if (this._msgTimer === null) { + Util.Debug("More data to process, creating timer"); + this._msgTimer = setTimeout(function () { + this._msgTimer = null; + this._handle_message(); + }.bind(this), 10); + } else { + Util.Debug("More data to process, existing timer"); + } + } + break; + default: + this._init_msg(); + break; + } + }, - var strlen, reason, length, sversion, cversion, repeaterID, - i, types, num_types, challenge, response, bpp, depth, - big_endian, red_max, green_max, blue_max, red_shift, - green_shift, blue_shift, true_color, name_length, is_repeater; + _checkEvents: function () { + if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) { + this._sock.send(this._mouse_arr); + this._mouse_arr = []; + } + }, - //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0)); - switch (rfb_state) { + _handleKeyPress: function (keysym, down) { + if (this._view_only) { return; } // View only, skip keyboard, events + this._sock.send(RFB.messages.keyEvent(keysym, down)); + }, - case 'ProtocolVersion' : - if (ws.rQlen() < 12) { - return fail("Incomplete protocol version"); - } - sversion = ws.rQshiftStr(12).substr(4,7); - Util.Info("Server ProtocolVersion: " + sversion); - is_repeater = 0; - switch (sversion) { - case "000.000": is_repeater = 1; break; // UltraVNC repeater - case "003.003": rfb_version = 3.3; break; - case "003.006": rfb_version = 3.3; break; // UltraVNC - case "003.889": rfb_version = 3.3; break; // Apple Remote Desktop - case "003.007": rfb_version = 3.7; break; - case "003.008": rfb_version = 3.8; break; - case "004.000": rfb_version = 3.8; break; // Intel AMT KVM - case "004.001": rfb_version = 3.8; break; // RealVNC 4.6 - default: - return fail("Invalid server version " + sversion); - } - if (is_repeater) { - repeaterID = conf.repeaterID; - while (repeaterID.length < 250) { - repeaterID += "\0"; + _handleMouseButton: function (x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMask ^= bmask; } - ws.send_string(repeaterID); - break; - } - if (rfb_version > rfb_max_version) { - rfb_version = rfb_max_version; - } - if (! test_mode) { - sendTimer = setInterval(function() { - // Send updates either at a rate of one update - // every 50ms, or whatever slower rate the network - // can handle. - ws.flush(); - }, 50); - } + if (this._viewportDrag) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': x, 'y': y}; - cversion = "00" + parseInt(rfb_version,10) + - ".00" + ((rfb_version * 10) % 10); - ws.send_string("RFB " + cversion + "\n"); - updateState('Security', "Sent ProtocolVersion: " + cversion); - break; - - case 'Security' : - if (rfb_version >= 3.7) { - // Server sends supported list, client decides - num_types = ws.rQshift8(); - if (ws.rQwait("security type", num_types, 1)) { return false; } - if (num_types === 0) { - strlen = ws.rQshift32(); - reason = ws.rQshiftStr(strlen); - return fail("Security failure: " + reason); - } - rfb_auth_scheme = 0; - types = ws.rQshiftBytes(num_types); - Util.Debug("Server security types: " + types); - for (i=0; i < types.length; i+=1) { - if ((types[i] > rfb_auth_scheme) && (types[i] < 3)) { - rfb_auth_scheme = types[i]; + // Skip sending mouse events + return; + } else { + this._viewportDragging = false; } } - if (rfb_auth_scheme === 0) { - return fail("Unsupported security types: " + types); + + if (this._view_only) { return; } // View only, skip mouse events + + this._mouse_arr = this._mouse_arr.concat( + RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); + this._sock.send(this._mouse_arr); + this._mouse_arr = []; + }, + + _handleMouseMove: function (x, y) { + if (this._viewportDragging) { + var deltaX = this._viewportDragPos.x - x; + var deltaY = this._viewportDragPos.y - y; + this._viewportDragPos = {'x': x, 'y': y}; + + this._display.viewportChange(deltaX, deltaY); + + // Skip sending mouse events + return; } - - ws.send([rfb_auth_scheme]); - } else { - // Server decides - if (ws.rQwait("security scheme", 4)) { return false; } - rfb_auth_scheme = ws.rQshift32(); - } - updateState('Authentication', - "Authenticating using scheme: " + rfb_auth_scheme); - init_msg(); // Recursive fallthrough (workaround JSLint complaint) - break; - - // Triggered by fallthough, not by server message - case 'Authentication' : - //Util.Debug("Security auth scheme: " + rfb_auth_scheme); - switch (rfb_auth_scheme) { - case 0: // connection failed - if (ws.rQwait("auth reason", 4)) { return false; } - strlen = ws.rQshift32(); - reason = ws.rQshiftStr(strlen); - return fail("Auth failure: " + reason); - case 1: // no authentication - if (rfb_version >= 3.8) { - updateState('SecurityResult'); - return; + + if (this._view_only) { return; } // View only, skip mouse events + + this._mouse_arr = this._mouse_arr.concat( + RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); + + this._checkEvents(); + }, + + // Message Handlers + + _negotiate_protocol_version: function () { + if (this._sock.rQlen() < 12) { + return this._fail("Incomplete protocol version"); + } + + var sversion = this._sock.rQshiftStr(12).substr(4, 7); + Util.Info("Server ProtocolVersion: " + sversion); + var is_repeater = 0; + switch (sversion) { + case "000.000": // UltraVNC repeater + is_repeater = 1; + break; + case "003.003": + case "003.006": // UltraVNC + case "003.889": // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case "003.007": + this._rfb_version = 3.7; + break; + case "003.008": + case "004.000": // Intel AMT KVM + case "004.001": // RealVNC 4.6 + this._rfb_version = 3.8; + break; + default: + return this._fail("Invalid server version " + sversion); + } + + if (is_repeater) { + var repeaterID = this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; } - // Fall through to ClientInitialisation - break; - case 2: // VNC authentication - if (rfb_password.length === 0) { - // Notify via both callbacks since it is kind of - // a RFB state change and a UI interface issue. - updateState('password', "Password Required"); - conf.onPasswordRequired(that); - return; + this._sock.send_string(repeaterID); + return true; + } + + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; + } + + // Send updates either at a rate of 1 update per 50ms, or + // whatever slower rate the network can handle + this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50); + + var cversion = "00" + parseInt(this._rfb_version, 10) + + ".00" + ((this._rfb_version * 10) % 10); + this._sock.send_string("RFB " + cversion + "\n"); + this._updateState('Security', 'Sent ProtocolVersion: ' + cversion); + }, + + _negotiate_security: function () { + if (this._rfb_version >= 3.7) { + // Server sends supported list, client decides + var num_types = this._sock.rQshift8(); + if (this._sock.rQwait("security type", num_types, 1)) { return false; } + + if (num_types === 0) { + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Security failure: " + reason); } - if (ws.rQwait("auth challenge", 16)) { return false; } - challenge = ws.rQshiftBytes(16); - //Util.Debug("Password: " + rfb_password); - //Util.Debug("Challenge: " + challenge + - // " (" + challenge.length + ")"); - response = genDES(rfb_password, challenge); - //Util.Debug("Response: " + response + - // " (" + response.length + ")"); - - //Util.Debug("Sending DES encrypted auth response"); - ws.send(response); - updateState('SecurityResult'); - return; - default: - fail("Unsupported auth scheme: " + rfb_auth_scheme); - return; - } - updateState('ClientInitialisation', "No auth required"); - init_msg(); // Recursive fallthrough (workaround JSLint complaint) - break; - - case 'SecurityResult' : - if (ws.rQwait("VNC auth response ", 4)) { return false; } - switch (ws.rQshift32()) { - case 0: // OK - // Fall through to ClientInitialisation - break; - case 1: // failed - if (rfb_version >= 3.8) { - length = ws.rQshift32(); - if (ws.rQwait("SecurityResult reason", length, 8)) { - return false; + + this._rfb_auth_scheme = 0; + var types = this._sock.rQshiftBytes(num_types); + Util.Debug("Server security types: " + types); + for (var i = 0; i < types.length; i++) { + if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) { + this._rfb_auth_scheme = types[i]; } - reason = ws.rQshiftStr(length); - fail(reason); - } else { - fail("Authentication failed"); } - return; - case 2: // too-many - return fail("Too many auth attempts"); - } - updateState('ClientInitialisation', "Authentication OK"); - init_msg(); // Recursive fallthrough (workaround JSLint complaint) - break; - - // Triggered by fallthough, not by server message - case 'ClientInitialisation' : - ws.send([conf.shared ? 1 : 0]); // ClientInitialisation - updateState('ServerInitialisation', "Authentication OK"); - break; - - case 'ServerInitialisation' : - if (ws.rQwait("server initialization", 24)) { return false; } - - /* Screen size */ - fb_width = ws.rQshift16(); - fb_height = ws.rQshift16(); - - /* PIXEL_FORMAT */ - bpp = ws.rQshift8(); - depth = ws.rQshift8(); - big_endian = ws.rQshift8(); - true_color = ws.rQshift8(); - - red_max = ws.rQshift16(); - green_max = ws.rQshift16(); - blue_max = ws.rQshift16(); - red_shift = ws.rQshift8(); - green_shift = ws.rQshift8(); - blue_shift = ws.rQshift8(); - ws.rQshiftStr(3); // padding - - Util.Info("Screen: " + fb_width + "x" + fb_height + - ", bpp: " + bpp + ", depth: " + depth + - ", big_endian: " + big_endian + - ", true_color: " + true_color + - ", red_max: " + red_max + - ", green_max: " + green_max + - ", blue_max: " + blue_max + - ", red_shift: " + red_shift + - ", green_shift: " + green_shift + - ", blue_shift: " + blue_shift); - - if (big_endian !== 0) { - Util.Warn("Server native endian is not little endian"); - } - if (red_shift !== 16) { - Util.Warn("Server native red-shift is not 16"); - } - if (blue_shift !== 0) { - Util.Warn("Server native blue-shift is not 0"); - } - /* Connection name/title */ - name_length = ws.rQshift32(); - fb_name = ws.rQshiftStr(name_length); - - if (conf.true_color && fb_name === "Intel(r) AMT KVM") - { - Util.Warn("Intel AMT KVM only support 8/16 bit depths. Disabling true color"); - conf.true_color = false; - } + if (this._rfb_auth_scheme === 0) { + return this._fail("Unsupported security types: " + types); + } - display.set_true_color(conf.true_color); - display.resize(fb_width, fb_height); - keyboard.grab(); - mouse.grab(); + this._sock.send([this._rfb_auth_scheme]); + } else { + // Server decides + if (this._sock.rQwait("security scheme", 4)) { return false; } + this._rfb_auth_scheme = this._sock.rQshift32(); + } - if (conf.true_color) { - fb_Bpp = 4; - fb_depth = 3; - } else { - fb_Bpp = 1; - fb_depth = 1; - } + this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme); + return this._init_msg(); // jump to authentication + }, + + // authentication + _negotiate_xvp_auth: function () { + var xvp_sep = this._xvp_password_sep; + var xvp_auth = this._rfb_password.split(xvp_sep); + if (xvp_auth.length < 3) { + this._updateState('password', 'XVP credentials required (user' + xvp_sep + + 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password); + this._onPasswordRequired(this); + return false; + } - response = pixelFormat(); - response = response.concat(clientEncodings()); - response = response.concat(fbUpdateRequests()); - timing.fbu_rt_start = (new Date()).getTime(); - timing.pixels = 0; - ws.send(response); - - /* Start pushing/polling */ - setTimeout(checkEvents, conf.check_rate); - - if (conf.encrypt) { - updateState('normal', "Connected (encrypted) to: " + fb_name); - } else { - updateState('normal', "Connected (unencrypted) to: " + fb_name); - } - break; - } - //Util.Debug("<< init_msg"); -}; - - -/* Normal RFB/VNC server message handler */ -normal_msg = function() { - //Util.Debug(">> normal_msg"); - - var ret = true, msg_type, length, text, - c, first_colour, num_colours, red, green, blue; - - if (FBU.rects > 0) { - msg_type = 0; - } else { - msg_type = ws.rQshift8(); - } - switch (msg_type) { - case 0: // FramebufferUpdate - ret = framebufferUpdate(); // false means need more data - break; - case 1: // SetColourMapEntries - Util.Debug("SetColourMapEntries"); - ws.rQshift8(); // Padding - first_colour = ws.rQshift16(); // First colour - num_colours = ws.rQshift16(); - if (ws.rQwait("SetColourMapEntries", num_colours*6, 6)) { return false; } - - for (c=0; c < num_colours; c+=1) { - red = ws.rQshift16(); - //Util.Debug("red before: " + red); - red = parseInt(red / 256, 10); - //Util.Debug("red after: " + red); - green = parseInt(ws.rQshift16() / 256, 10); - blue = parseInt(ws.rQshift16() / 256, 10); - display.set_colourMap([blue, green, red], first_colour + c); - } - Util.Debug("colourMap: " + display.get_colourMap()); - Util.Info("Registered " + num_colours + " colourMap entries"); - //Util.Debug("colourMap: " + display.get_colourMap()); - break; - case 2: // Bell - Util.Debug("Bell"); - conf.onBell(that); - break; - case 3: // ServerCutText - Util.Debug("ServerCutText"); - if (ws.rQwait("ServerCutText header", 7, 1)) { return false; } - ws.rQshiftBytes(3); // Padding - length = ws.rQshift32(); - if (ws.rQwait("ServerCutText", length, 8)) { return false; } - - text = ws.rQshiftStr(length); - conf.clipboardReceive(that, text); // Obsolete - conf.onClipboard(that, text); - break; - default: - fail("Disconnected: illegal server message type " + msg_type); - Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); - break; - } - //Util.Debug("<< normal_msg"); - return ret; -}; - -framebufferUpdate = function() { - var now, hdr, fbu_rt_diff, ret = true; - - if (FBU.rects === 0) { - //Util.Debug("New FBU: ws.rQslice(0,20): " + ws.rQslice(0,20)); - if (ws.rQwait("FBU header", 3)) { - ws.rQunshift8(0); // FBU msg_type - return false; - } - ws.rQshift8(); // padding - FBU.rects = ws.rQshift16(); - //Util.Debug("FramebufferUpdate, rects:" + FBU.rects); - FBU.bytes = 0; - timing.cur_fbu = 0; - if (timing.fbu_rt_start > 0) { - now = (new Date()).getTime(); - Util.Info("First FBU latency: " + (now - timing.fbu_rt_start)); - } - } + var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + + String.fromCharCode(xvp_auth[1].length) + + xvp_auth[0] + + xvp_auth[1]; + this._sock.send_string(xvp_auth_str); + this._rfb_password = xvp_auth.slice(2).join(xvp_sep); + this._rfb_auth_scheme = 2; + return this._negotiate_authentication(); + }, + + _negotiate_std_vnc_auth: function () { + if (this._rfb_password.length === 0) { + // Notify via both callbacks since it's kind of + // an RFB state change and a UI interface issue + this._updateState('password', "Password Required"); + this._onPasswordRequired(this); + } - while (FBU.rects > 0) { - if (rfb_state !== "normal") { - return false; - } - if (ws.rQwait("FBU", FBU.bytes)) { return false; } - if (FBU.bytes === 0) { - if (ws.rQwait("rect header", 12)) { return false; } - /* New FramebufferUpdate */ - - hdr = ws.rQshiftBytes(12); - FBU.x = (hdr[0] << 8) + hdr[1]; - FBU.y = (hdr[2] << 8) + hdr[3]; - FBU.width = (hdr[4] << 8) + hdr[5]; - FBU.height = (hdr[6] << 8) + hdr[7]; - FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + - (hdr[10] << 8) + hdr[11], 10); - - conf.onFBUReceive(that, - {'x': FBU.x, 'y': FBU.y, - 'width': FBU.width, 'height': FBU.height, - 'encoding': FBU.encoding, - 'encodingName': encNames[FBU.encoding]}); - - if (encNames[FBU.encoding]) { - // Debug: - /* - var msg = "FramebufferUpdate rects:" + FBU.rects; - msg += " x: " + FBU.x + " y: " + FBU.y; - msg += " width: " + FBU.width + " height: " + FBU.height; - msg += " encoding:" + FBU.encoding; - msg += "(" + encNames[FBU.encoding] + ")"; - msg += ", ws.rQlen(): " + ws.rQlen(); - Util.Debug(msg); - */ + if (this._sock.rQwait("auth challenge", 16)) { return false; } + + var challenge = this._sock.rQshiftBytes(16); + var response = RFB.genDES(this._rfb_password, challenge); + this._sock.send(response); + this._updateState("SecurityResult"); + return true; + }, + + _negotiate_tight_tunnels: function (numTunnels) { + var clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } + }; + var serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (var i = 0; i < numTunnels; i++) { + var cap_code = this._sock.rQshift32(); + var cap_vendor = this._sock.rQshiftStr(4); + var cap_signature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || + serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + return this._fail("Client's tunnel type had the incorrect vendor or signature"); + } + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue } else { - fail("Disconnected: unsupported encoding " + - FBU.encoding); - return false; + return this._fail("Server wanted tunnels, but doesn't support the notunnel type"); } - } + }, - timing.last_fbu = (new Date()).getTime(); + _negotiate_tight_auth: function () { + if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation + if (this._sock.rQwait("num tunnels", 4)) { return false; } + var numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } - ret = encHandlers[FBU.encoding](); + this._rfb_tightvnc = true; - now = (new Date()).getTime(); - timing.cur_fbu += (now - timing.last_fbu); + if (numTunnels > 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue + } + } - if (ret) { - encStats[FBU.encoding][0] += 1; - encStats[FBU.encoding][1] += 1; - timing.pixels += FBU.width * FBU.height; - } + // second pass, do the sub-auth negotiation + if (this._sock.rQwait("sub auth count", 4)) { return false; } + var subAuthCount = this._sock.rQshift32(); + if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } - if (timing.pixels >= (fb_width * fb_height)) { - if (((FBU.width === fb_width) && - (FBU.height === fb_height)) || - (timing.fbu_rt_start > 0)) { - timing.full_fbu_total += timing.cur_fbu; - timing.full_fbu_cnt += 1; - Util.Info("Timing of full FBU, cur: " + - timing.cur_fbu + ", total: " + - timing.full_fbu_total + ", cnt: " + - timing.full_fbu_cnt + ", avg: " + - (timing.full_fbu_total / - timing.full_fbu_cnt)); - } - if (timing.fbu_rt_start > 0) { - fbu_rt_diff = now - timing.fbu_rt_start; - timing.fbu_rt_total += fbu_rt_diff; - timing.fbu_rt_cnt += 1; - Util.Info("full FBU round-trip, cur: " + - fbu_rt_diff + ", total: " + - timing.fbu_rt_total + ", cnt: " + - timing.fbu_rt_cnt + ", avg: " + - (timing.fbu_rt_total / - timing.fbu_rt_cnt)); - timing.fbu_rt_start = 0; + var clientSupportedTypes = { + 'STDVNOAUTH__': 1, + 'STDVVNCAUTH_': 2 + }; + + var serverSupportedTypes = []; + + for (var i = 0; i < subAuthCount; i++) { + var capNum = this._sock.rQshift32(); + var capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); } - } - if (! ret) { - return ret; // false ret means need more data - } - } - - conf.onFBUComplete(that, - {'x': FBU.x, 'y': FBU.y, - 'width': FBU.width, 'height': FBU.height, - 'encoding': FBU.encoding, - 'encodingName': encNames[FBU.encoding]}); - - return true; // We finished this FBU -}; - -// -// FramebufferUpdate encodings -// - -encHandlers.RAW = function display_raw() { - //Util.Debug(">> display_raw (" + ws.rQlen() + " bytes)"); - - var cur_y, cur_height; - - if (FBU.lines === 0) { - FBU.lines = FBU.height; - } - FBU.bytes = FBU.width * fb_Bpp; // At least a line - if (ws.rQwait("RAW", FBU.bytes)) { return false; } - cur_y = FBU.y + (FBU.height - FBU.lines); - cur_height = Math.min(FBU.lines, - Math.floor(ws.rQlen()/(FBU.width * fb_Bpp))); - display.blitImage(FBU.x, cur_y, FBU.width, cur_height, - ws.get_rQ(), ws.get_rQi()); - ws.rQshiftBytes(FBU.width * cur_height * fb_Bpp); - FBU.lines -= cur_height; - - if (FBU.lines > 0) { - FBU.bytes = FBU.width * fb_Bpp; // At least another line - } else { - FBU.rects -= 1; - FBU.bytes = 0; - } - //Util.Debug("<< display_raw (" + ws.rQlen() + " bytes)"); - return true; -}; - -encHandlers.COPYRECT = function display_copy_rect() { - //Util.Debug(">> display_copy_rect"); - - var old_x, old_y; - - if (ws.rQwait("COPYRECT", 4)) { return false; } - display.renderQ_push({ - 'type': 'copy', - 'old_x': ws.rQshift16(), - 'old_y': ws.rQshift16(), - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height}); - FBU.rects -= 1; - FBU.bytes = 0; - return true; -}; - -encHandlers.RRE = function display_rre() { - //Util.Debug(">> display_rre (" + ws.rQlen() + " bytes)"); - var color, x, y, width, height, chunk; - - if (FBU.subrects === 0) { - if (ws.rQwait("RRE", 4+fb_Bpp)) { return false; } - FBU.subrects = ws.rQshift32(); - color = ws.rQshiftBytes(fb_Bpp); // Background - display.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); - } - while ((FBU.subrects > 0) && (ws.rQlen() >= (fb_Bpp + 8))) { - color = ws.rQshiftBytes(fb_Bpp); - x = ws.rQshift16(); - y = ws.rQshift16(); - width = ws.rQshift16(); - height = ws.rQshift16(); - display.fillRect(FBU.x + x, FBU.y + y, width, height, color); - FBU.subrects -= 1; - } - //Util.Debug(" display_rre: rects: " + FBU.rects + - // ", FBU.subrects: " + FBU.subrects); - - if (FBU.subrects > 0) { - chunk = Math.min(rre_chunk_sz, FBU.subrects); - FBU.bytes = (fb_Bpp + 8) * chunk; - } else { - FBU.rects -= 1; - FBU.bytes = 0; - } - //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes); - return true; -}; - -encHandlers.HEXTILE = function display_hextile() { - //Util.Debug(">> display_hextile"); - var subencoding, subrects, color, cur_tile, - tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh, - rQ = ws.get_rQ(), rQi = ws.get_rQi(); - - if (FBU.tiles === 0) { - FBU.tiles_x = Math.ceil(FBU.width/16); - FBU.tiles_y = Math.ceil(FBU.height/16); - FBU.total_tiles = FBU.tiles_x * FBU.tiles_y; - FBU.tiles = FBU.total_tiles; - } - - /* FBU.bytes comes in as 1, ws.rQlen() at least 1 */ - while (FBU.tiles > 0) { - FBU.bytes = 1; - if (ws.rQwait("HEXTILE subencoding", FBU.bytes)) { return false; } - subencoding = rQ[rQi]; // Peek - if (subencoding > 30) { // Raw - fail("Disconnected: illegal hextile subencoding " + subencoding); - //Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); - return false; - } - subrects = 0; - cur_tile = FBU.total_tiles - FBU.tiles; - tile_x = cur_tile % FBU.tiles_x; - tile_y = Math.floor(cur_tile / FBU.tiles_x); - x = FBU.x + tile_x * 16; - y = FBU.y + tile_y * 16; - w = Math.min(16, (FBU.x + FBU.width) - x); - h = Math.min(16, (FBU.y + FBU.height) - y); - - /* Figure out how much we are expecting */ - if (subencoding & 0x01) { // Raw - //Util.Debug(" Raw subencoding"); - FBU.bytes += w * h * fb_Bpp; - } else { - if (subencoding & 0x02) { // Background - FBU.bytes += fb_Bpp; - } - if (subencoding & 0x04) { // Foreground - FBU.bytes += fb_Bpp; - } - if (subencoding & 0x08) { // AnySubrects - FBU.bytes += 1; // Since we aren't shifting it off - if (ws.rQwait("hextile subrects header", FBU.bytes)) { return false; } - subrects = rQ[rQi + FBU.bytes-1]; // Peek - if (subencoding & 0x10) { // SubrectsColoured - FBU.bytes += subrects * (fb_Bpp + 2); - } else { - FBU.bytes += subrects * 2; + + for (var authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + + switch (authType) { + case 'STDVNOAUTH__': // no auth + this._updateState('SecurityResult'); + return true; + case 'STDVVNCAUTH_': // VNC auth + this._rfb_auth_scheme = 2; + return this._init_msg(); + default: + return this._fail("Unsupported tiny auth scheme: " + authType); + } } } - } - /* - Util.Debug(" tile:" + cur_tile + "/" + (FBU.total_tiles - 1) + - " (" + tile_x + "," + tile_y + ")" + - " [" + x + "," + y + "]@" + w + "x" + h + - ", subenc:" + subencoding + - "(last: " + FBU.lastsubencoding + "), subrects:" + - subrects + - ", ws.rQlen():" + ws.rQlen() + ", FBU.bytes:" + FBU.bytes + - " last:" + ws.rQslice(FBU.bytes-10, FBU.bytes) + - " next:" + ws.rQslice(FBU.bytes-1, FBU.bytes+10)); - */ - if (ws.rQwait("hextile", FBU.bytes)) { return false; } - - /* We know the encoding and have a whole tile */ - FBU.subencoding = rQ[rQi]; - rQi += 1; - if (FBU.subencoding === 0) { - if (FBU.lastsubencoding & 0x01) { - /* Weird: ignore blanks after RAW */ - Util.Debug(" Ignoring blank after RAW"); - } else { - display.fillRect(x, y, w, h, FBU.background); + this._fail("No supported sub-auth types!"); + }, + + _negotiate_authentication: function () { + switch (this._rfb_auth_scheme) { + case 0: // connection failed + if (this._sock.rQwait("auth reason", 4)) { return false; } + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Auth failure: " + reason); + + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._updateState('SecurityResult'); + return true; + } + this._updateState('ClientInitialisation', "No auth required"); + return this._init_msg(); + + case 22: // XVP auth + return this._negotiate_xvp_auth(); + + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); + + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); + + default: + return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme); } - } else if (FBU.subencoding & 0x01) { // Raw - display.blitImage(x, y, w, h, rQ, rQi); - rQi += FBU.bytes - 1; - } else { - if (FBU.subencoding & 0x02) { // Background - FBU.background = rQ.slice(rQi, rQi + fb_Bpp); - rQi += fb_Bpp; - } - if (FBU.subencoding & 0x04) { // Foreground - FBU.foreground = rQ.slice(rQi, rQi + fb_Bpp); - rQi += fb_Bpp; - } - - display.startTile(x, y, w, h, FBU.background); - if (FBU.subencoding & 0x08) { // AnySubrects - subrects = rQ[rQi]; - rQi += 1; - for (s = 0; s < subrects; s += 1) { - if (FBU.subencoding & 0x10) { // SubrectsColoured - color = rQ.slice(rQi, rQi + fb_Bpp); - rQi += fb_Bpp; + }, + + _handle_security_result: function () { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } + switch (this._sock.rQshift32()) { + case 0: // OK + this._updateState('ClientInitialisation', 'Authentication OK'); + return this._init_msg(); + case 1: // failed + if (this._rfb_version >= 3.8) { + var length = this._sock.rQshift32(); + if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } + var reason = this._sock.rQshiftStr(length); + return this._fail(reason); } else { - color = FBU.foreground; + return this._fail("Authentication failure"); } - xy = rQ[rQi]; - rQi += 1; - sx = (xy >> 4); - sy = (xy & 0x0f); + return false; + case 2: + return this._fail("Too many auth attempts"); + } + }, + + _negotiate_server_init: function () { + if (this._sock.rQwait("server initialization", 24)) { return false; } + + /* Screen size */ + this._fb_width = this._sock.rQshift16(); + this._fb_height = this._sock.rQshift16(); + + /* PIXEL_FORMAT */ + var bpp = this._sock.rQshift8(); + var depth = this._sock.rQshift8(); + var big_endian = this._sock.rQshift8(); + var true_color = this._sock.rQshift8(); + + var red_max = this._sock.rQshift16(); + var green_max = this._sock.rQshift16(); + var blue_max = this._sock.rQshift16(); + var red_shift = this._sock.rQshift8(); + var green_shift = this._sock.rQshift8(); + var blue_shift = this._sock.rQshift8(); + this._sock.rQskipBytes(3); // padding + + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack + + /* Connection name/title */ + var name_length = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', name_length, 24)) { return false; } + this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); + + if (this._rfb_tightvnc) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } + // In TightVNC mode, ServerInit message is extended + var numServerMessages = this._sock.rQshift16(); + var numClientMessages = this._sock.rQshift16(); + var numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding + + var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } + + var i; + for (i = 0; i < numServerMessages; i++) { + var srvMsg = this._sock.rQshiftStr(16); + } - wh = rQ[rQi]; - rQi += 1; - sw = (wh >> 4) + 1; - sh = (wh & 0x0f) + 1; + for (i = 0; i < numClientMessages; i++) { + var clientMsg = this._sock.rQshiftStr(16); + } - display.subTile(sx, sy, sw, sh, color); + for (i = 0; i < numEncodings; i++) { + var encoding = this._sock.rQshiftStr(16); } } - display.finishTile(); - } - ws.set_rQi(rQi); - FBU.lastsubencoding = FBU.subencoding; - FBU.bytes = 0; - FBU.tiles -= 1; - } - - if (FBU.tiles === 0) { - FBU.rects -= 1; - } - - //Util.Debug("<< display_hextile"); - return true; -}; - - -// Get 'compact length' header and data size -getTightCLength = function (arr) { - var header = 1, data = 0; - data += arr[0] & 0x7f; - if (arr[0] & 0x80) { - header += 1; - data += (arr[1] & 0x7f) << 7; - if (arr[1] & 0x80) { - header += 1; - data += arr[2] << 14; - } - } - return [header, data]; -}; -function display_tight(isTightPNG) { - //Util.Debug(">> display_tight"); + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + + ", bpp: " + bpp + ", depth: " + depth + + ", big_endian: " + big_endian + + ", true_color: " + true_color + + ", red_max: " + red_max + + ", green_max: " + green_max + + ", blue_max: " + blue_max + + ", red_shift: " + red_shift + + ", green_shift: " + green_shift + + ", blue_shift: " + blue_shift); + + if (big_endian !== 0) { + Util.Warn("Server native endian is not little endian"); + } - if (fb_depth === 1) { - fail("Tight protocol handler only implements true color mode"); - } + if (red_shift !== 16) { + Util.Warn("Server native red-shift is not 16"); + } - var ctl, cmode, clength, color, img, data; - var filterId = -1, resetStreams = 0, streamId = -1; - var rQ = ws.get_rQ(), rQi = ws.get_rQi(); + if (blue_shift !== 0) { + Util.Warn("Server native blue-shift is not 0"); + } - FBU.bytes = 1; // compression-control byte - if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; } + // we're past the point where we could backtrack, so it's safe to call this + this._onDesktopName(this, this._fb_name); - var checksum = function(data) { - var sum=0, i; - for (i=0; i 65536) sum -= 65536; - } - return sum; - } + if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { + Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); + this._true_color = false; + } - var decompress = function(data) { - for (var i=0; i<4; i++) { - if ((resetStreams >> i) & 1) { - FBU.zlibs[i].reset(); - Util.Info("Reset zlib stream " + i); + this._display.set_true_color(this._true_color); + this._onFBResize(this, this._fb_width, this._fb_height); + this._display.resize(this._fb_width, this._fb_height); + this._keyboard.grab(); + this._mouse.grab(); + + if (this._true_color) { + this._fb_Bpp = 4; + this._fb_depth = 3; + } else { + this._fb_Bpp = 1; + this._fb_depth = 1; } - } - var uncompressed = FBU.zlibs[streamId].uncompress(data, 0); - if (uncompressed.status !== 0) { - Util.Error("Invalid data in zlib stream"); - } - //Util.Warn("Decompressed " + data.length + " to " + - // uncompressed.data.length + " checksums " + - // checksum(data) + ":" + checksum(uncompressed.data)); - - return uncompressed.data; - } - - var handlePalette = function() { - var numColors = rQ[rQi + 2] + 1; - var paletteSize = numColors * fb_depth; - FBU.bytes += paletteSize; - if (ws.rQwait("TIGHT palette " + cmode, FBU.bytes)) { return false; } - - var bpp = (numColors <= 2) ? 1 : 8; - var rowSize = Math.floor((FBU.width * bpp + 7) / 8); - var raw = false; - if (rowSize * FBU.height < 12) { - raw = true; - clength = [0, rowSize * FBU.height]; - } else { - clength = getTightCLength(ws.rQslice(3 + paletteSize, - 3 + paletteSize + 3)); - } - FBU.bytes += clength[0] + clength[1]; - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } - // Shift ctl, filter id, num colors, palette entries, and clength off - ws.rQshiftBytes(3); - var palette = ws.rQshiftBytes(paletteSize); - ws.rQshiftBytes(clength[0]); + var response = RFB.messages.pixelFormat(this._fb_Bpp, this._fb_depth, this._true_color); + response = response.concat( + RFB.messages.clientEncodings(this._encodings, this._local_cursor, this._true_color)); + response = response.concat( + RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(), + this._fb_width, this._fb_height)); - if (raw) { - data = ws.rQshiftBytes(clength[1]); - } else { - data = decompress(ws.rQshiftBytes(clength[1])); - } + this._timing.fbu_rt_start = (new Date()).getTime(); + this._timing.pixels = 0; + this._sock.send(response); + + this._checkEvents(); + + if (this._encrypt) { + this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); + } else { + this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name); + } + }, + + _init_msg: function () { + switch (this._rfb_state) { + case 'ProtocolVersion': + return this._negotiate_protocol_version(); + + case 'Security': + return this._negotiate_security(); + + case 'Authentication': + return this._negotiate_authentication(); + + case 'SecurityResult': + return this._handle_security_result(); + + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._updateState('ServerInitialisation', "Authentication OK"); + return true; + + case 'ServerInitialisation': + return this._negotiate_server_init(); + } + }, + + _handle_set_colour_map_msg: function () { + Util.Debug("SetColorMapEntries"); + this._sock.rQskip8(); // Padding + + var first_colour = this._sock.rQshift16(); + var num_colours = this._sock.rQshift16(); + if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; } + + for (var c = 0; c < num_colours; c++) { + var red = parseInt(this._sock.rQshift16() / 256, 10); + var green = parseInt(this._sock.rQshift16() / 256, 10); + var blue = parseInt(this._sock.rQshift16() / 256, 10); + this._display.set_colourMap([blue, green, red], first_colour + c); + } + Util.Debug("colourMap: " + this._display.get_colourMap()); + Util.Info("Registered " + num_colours + " colourMap entries"); + + return true; + }, + + _handle_server_cut_text: function () { + Util.Debug("ServerCutText"); + if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + var length = this._sock.rQshift32(); + if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } + + var text = this._sock.rQshiftStr(length); + this._onClipboard(this, text); + + return true; + }, + + _handle_xvp_msg: function () { + if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + var xvp_ver = this._sock.rQshift8(); + var xvp_msg = this._sock.rQshift8(); + + switch (xvp_msg) { + case 0: // XVP_FAIL + this._updateState(this._rfb_state, "Operation Failed"); + break; + case 1: // XVP_INIT + this._rfb_xvp_ver = xvp_ver; + Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); + this._onXvpInit(this._rfb_xvp_ver); + break; + default: + this._fail("Disconnected: illegal server XVP message " + xvp_msg); + break; + } + + return true; + }, - // Convert indexed (palette based) image data to RGB - // TODO: reduce number of calculations inside loop - var dest = []; - var x, y, b, w, w1, dp, sp; - if (numColors === 2) { - w = Math.floor((FBU.width + 7) / 8); - w1 = Math.floor(FBU.width / 8); - for (y = 0; y < FBU.height; y++) { - for (x = 0; x < w1; x++) { - for (b = 7; b >= 0; b--) { - dp = (y*FBU.width + x*8 + 7-b) * 3; - sp = (data[y*w + x] >> b & 1) * 3; - dest[dp ] = palette[sp ]; - dest[dp+1] = palette[sp+1]; - dest[dp+2] = palette[sp+2]; + _normal_msg: function () { + var msg_type; + + if (this._FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = this._sock.rQshift8(); + } + + switch (msg_type) { + case 0: // FramebufferUpdate + var ret = this._framebufferUpdate(); + if (ret) { + this._sock.send(RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(), + this._fb_width, this._fb_height)); } - } - for (b = 7; b >= 8 - FBU.width % 8; b--) { - dp = (y*FBU.width + x*8 + 7-b) * 3; - sp = (data[y*w + x] >> b & 1) * 3; - dest[dp ] = palette[sp ]; - dest[dp+1] = palette[sp+1]; - dest[dp+2] = palette[sp+2]; + return ret; + + case 1: // SetColorMapEntries + return this._handle_set_colour_map_msg(); + + case 2: // Bell + Util.Debug("Bell"); + this._onBell(this); + return true; + + case 3: // ServerCutText + return this._handle_server_cut_text(); + + case 250: // XVP + return this._handle_xvp_msg(); + + default: + this._fail("Disconnected: illegal server message type " + msg_type); + Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + return true; + } + }, + + _framebufferUpdate: function () { + var ret = true; + var now; + + if (this._FBU.rects === 0) { + if (this._sock.rQwait("FBU header", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + this._FBU.rects = this._sock.rQshift16(); + this._FBU.bytes = 0; + this._timing.cur_fbu = 0; + if (this._timing.fbu_rt_start > 0) { + now = (new Date()).getTime(); + Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); } } - } else { - for (y = 0; y < FBU.height; y++) { - for (x = 0; x < FBU.width; x++) { - dp = (y*FBU.width + x) * 3; - sp = data[y*FBU.width + x] * 3; - dest[dp ] = palette[sp ]; - dest[dp+1] = palette[sp+1]; - dest[dp+2] = palette[sp+2]; + + while (this._FBU.rects > 0) { + if (this._rfb_state !== "normal") { return false; } + + if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } + if (this._FBU.bytes === 0) { + if (this._sock.rQwait("rect header", 12)) { return false; } + /* New FramebufferUpdate */ + + var hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + + (hdr[10] << 8) + hdr[11], 10); + + this._onFBUReceive(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + if (!this._encNames[this._FBU.encoding]) { + this._fail("Disconnected: unsupported encoding " + + this._FBU.encoding); + return false; + } + } + + this._timing.last_fbu = (new Date()).getTime(); + + ret = this._encHandlers[this._FBU.encoding](); + + now = (new Date()).getTime(); + this._timing.cur_fbu += (now - this._timing.last_fbu); + + if (ret) { + this._encStats[this._FBU.encoding][0]++; + this._encStats[this._FBU.encoding][1]++; + this._timing.pixels += this._FBU.width * this._FBU.height; + } + + if (this._timing.pixels >= (this._fb_width * this._fb_height)) { + if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || + this._timing.fbu_rt_start > 0) { + this._timing.full_fbu_total += this._timing.cur_fbu; + this._timing.full_fbu_cnt++; + Util.Info("Timing of full FBU, curr: " + + this._timing.cur_fbu + ", total: " + + this._timing.full_fbu_total + ", cnt: " + + this._timing.full_fbu_cnt + ", avg: " + + (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); + } + + if (this._timing.fbu_rt_start > 0) { + var fbu_rt_diff = now - this._timing.fbu_rt_start; + this._timing.fbu_rt_total += fbu_rt_diff; + this._timing.fbu_rt_cnt++; + Util.Info("full FBU round-trip, cur: " + + fbu_rt_diff + ", total: " + + this._timing.fbu_rt_total + ", cnt: " + + this._timing.fbu_rt_cnt + ", avg: " + + (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); + this._timing.fbu_rt_start = 0; + } } + + if (!ret) { return ret; } // need more data } - } - display.renderQ_push({ - 'type': 'blitRgb', - 'data': dest, - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height}); - return true; - } - - var handleCopy = function() { - var raw = false; - var uncompressedSize = FBU.width * FBU.height * fb_depth; - if (uncompressedSize < 12) { - raw = true; - clength = [0, uncompressedSize]; + this._onFBUComplete(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + return true; // We finished this FBU + }, + }; + + Util.make_properties(RFB, [ + ['target', 'wo', 'dom'], // VNC display rendering Canvas object + ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input + ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption + ['true_color', 'rw', 'bool'], // Request true color pixel data + ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor + ['shared', 'rw', 'bool'], // Request shared mode + ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard + ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields + ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection + ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection + ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to + ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags + + // Callback functions + ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change + ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required + ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received + ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received + ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed + ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized + ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received + ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection + ]); + + RFB.prototype.set_local_cursor = function (cursor) { + if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { + this._local_cursor = false; } else { - clength = getTightCLength(ws.rQslice(1, 4)); + if (this._display.get_cursor_uri()) { + this._local_cursor = true; + } else { + Util.Warn("Browser does not support local cursor"); + } } - FBU.bytes = 1 + clength[0] + clength[1]; - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } + }; + + RFB.prototype.get_display = function () { return this._display; }; + RFB.prototype.get_keyboard = function () { return this._keyboard; }; + RFB.prototype.get_mouse = function () { return this._mouse; }; + + // Class Methods + RFB.messages = { + keyEvent: function (keysym, down) { + var arr = [4]; + arr.push8(down); + arr.push16(0); + arr.push32(keysym); + return arr; + }, + + pointerEvent: function (x, y, mask) { + var arr = [5]; // msg-type + arr.push8(mask); + arr.push16(x); + arr.push16(y); + return arr; + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText: function (text) { + var arr = [6]; // msg-type + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + arr.push32(text.length); + var n = text.length; + for (var i = 0; i < n; i++) { + arr.push(text.charCodeAt(i)); + } - // Shift ctl, clength off - ws.rQshiftBytes(1 + clength[0]); + return arr; + }, + + pixelFormat: function (bpp, depth, true_color) { + var arr = [0]; // msg-type + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + + arr.push8(bpp * 8); // bits-per-pixel + arr.push8(depth * 8); // depth + arr.push8(0); // little-endian + arr.push8(true_color ? 1 : 0); // true-color + + arr.push16(255); // red-max + arr.push16(255); // green-max + arr.push16(255); // blue-max + arr.push8(16); // red-shift + arr.push8(8); // green-shift + arr.push8(0); // blue-shift + + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + return arr; + }, + + clientEncodings: function (encodings, local_cursor, true_color) { + var i, encList = []; + + for (i = 0; i < encodings.length; i++) { + if (encodings[i][0] === "Cursor" && !local_cursor) { + Util.Debug("Skipping Cursor pseudo-encoding"); + } else if (encodings[i][0] === "TIGHT" && !true_color) { + // TODO: remove this when we have tight+non-true-color + Util.Warn("Skipping tight as it is only supported with true color"); + } else { + encList.push(encodings[i][1]); + } + } - if (raw) { - data = ws.rQshiftBytes(clength[1]); - } else { - data = decompress(ws.rQshiftBytes(clength[1])); + var arr = [2]; // msg-type + arr.push8(0); // padding + + arr.push16(encList.length); // encoding count + for (i = 0; i < encList.length; i++) { + arr.push32(encList[i]); + } + + return arr; + }, + + fbUpdateRequests: function (cleanDirty, fb_width, fb_height) { + var arr = []; + + var cb = cleanDirty.cleanBox; + var w, h; + if (cb.w > 0 && cb.h > 0) { + w = typeof cb.w === "undefined" ? fb_width : cb.w; + h = typeof cb.h === "undefined" ? fb_height : cb.h; + // Request incremental for clean box + arr = arr.concat(RFB.messages.fbUpdateRequest(1, cb.x, cb.y, w, h)); + } + + for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) { + var db = cleanDirty.dirtyBoxes[i]; + // Force all (non-incremental) for dirty box + w = typeof db.w === "undefined" ? fb_width : db.w; + h = typeof db.h === "undefined" ? fb_height : db.h; + arr = arr.concat(RFB.messages.fbUpdateRequest(0, db.x, db.y, w, h)); + } + + return arr; + }, + + fbUpdateRequest: function (incremental, x, y, w, h) { + if (typeof(x) === "undefined") { x = 0; } + if (typeof(y) === "undefined") { y = 0; } + + var arr = [3]; // msg-type + arr.push8(incremental); + arr.push16(x); + arr.push16(y); + arr.push16(w); + arr.push16(h); + + return arr; } + }; - display.renderQ_push({ - 'type': 'blitRgb', - 'data': data, - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height}); - return true; - } - - ctl = ws.rQpeek8(); - - // Keep tight reset bits - resetStreams = ctl & 0xF; - - // Figure out filter - ctl = ctl >> 4; - streamId = ctl & 0x3; - - if (ctl === 0x08) cmode = "fill"; - else if (ctl === 0x09) cmode = "jpeg"; - else if (ctl === 0x0A) cmode = "png"; - else if (ctl & 0x04) cmode = "filter"; - else if (ctl < 0x04) cmode = "copy"; - else return fail("Illegal tight compression received, ctl: " + ctl); - - if (isTightPNG && (cmode === "filter" || cmode === "copy")) { - return fail("filter/copy received in tightPNG mode"); - } - - switch (cmode) { - // fill uses fb_depth because TPIXELs drop the padding byte - case "fill": FBU.bytes += fb_depth; break; // TPIXEL - case "jpeg": FBU.bytes += 3; break; // max clength - case "png": FBU.bytes += 3; break; // max clength - case "filter": FBU.bytes += 2; break; // filter id + num colors if palette - case "copy": break; - } - - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } - - //Util.Debug(" ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); - //Util.Debug(" cmode: " + cmode); - - // Determine FBU.bytes - switch (cmode) { - case "fill": - ws.rQshift8(); // shift off ctl - color = ws.rQshiftBytes(fb_depth); - display.renderQ_push({ - 'type': 'fill', - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height, - 'color': [color[2], color[1], color[0]] }); - break; - case "png": - case "jpeg": - clength = getTightCLength(ws.rQslice(1, 4)); - FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } - - // We have everything, render it - //Util.Debug(" jpeg, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " + - // clength[0] + ", clength[1]: " + clength[1]); - ws.rQshiftBytes(1 + clength[0]); // shift off ctl + compact length - img = new Image(); - img.src = "data:image/" + cmode + - extract_data_uri(ws.rQshiftBytes(clength[1])); - display.renderQ_push({ - 'type': 'img', - 'img': img, - 'x': FBU.x, - 'y': FBU.y}); - img = null; - break; - case "filter": - filterId = rQ[rQi + 1]; - if (filterId === 1) { - if (!handlePalette()) { return false; } - } else { - // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter - // Filter 2, Gradient is valid but not used if jpeg is enabled - throw("Unsupported tight subencoding received, filter: " + filterId); + RFB.genDES = function (password, challenge) { + var passwd = []; + for (var i = 0; i < password.length; i++) { + passwd.push(password.charCodeAt(i)); } - break; - case "copy": - if (!handleCopy()) { return false; } - break; - } - - FBU.bytes = 0; - FBU.rects -= 1; - //Util.Debug(" ending ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); - //Util.Debug("<< display_tight_png"); - return true; -} - -extract_data_uri = function(arr) { - //var i, stra = []; - //for (i=0; i< arr.length; i += 1) { - // stra.push(String.fromCharCode(arr[i])); - //} - //return "," + escape(stra.join('')); - return ";base64," + Base64.encode(arr); -}; - -encHandlers.TIGHT = function () { return display_tight(false); }; -encHandlers.TIGHT_PNG = function () { return display_tight(true); }; - -encHandlers.last_rect = function last_rect() { - //Util.Debug(">> last_rect"); - FBU.rects = 0; - //Util.Debug("<< last_rect"); - return true; -}; - -encHandlers.DesktopSize = function set_desktopsize() { - Util.Debug(">> set_desktopsize"); - fb_width = FBU.width; - fb_height = FBU.height; - display.resize(fb_width, fb_height); - timing.fbu_rt_start = (new Date()).getTime(); - // Send a new non-incremental request - ws.send(fbUpdateRequests()); - - FBU.bytes = 0; - FBU.rects -= 1; - - Util.Debug("<< set_desktopsize"); - return true; -}; - -encHandlers.Cursor = function set_cursor() { - var x, y, w, h, pixelslength, masklength; - Util.Debug(">> set_cursor"); - x = FBU.x; // hotspot-x - y = FBU.y; // hotspot-y - w = FBU.width; - h = FBU.height; - - pixelslength = w * h * fb_Bpp; - masklength = Math.floor((w + 7) / 8) * h; - - FBU.bytes = pixelslength + masklength; - if (ws.rQwait("cursor encoding", FBU.bytes)) { return false; } - - //Util.Debug(" set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h); - - display.changeCursor(ws.rQshiftBytes(pixelslength), - ws.rQshiftBytes(masklength), - x, y, w, h); - - FBU.bytes = 0; - FBU.rects -= 1; - - Util.Debug("<< set_cursor"); - return true; -}; - -encHandlers.JPEG_quality_lo = function set_jpeg_quality() { - Util.Error("Server sent jpeg_quality pseudo-encoding"); -}; - -encHandlers.compress_lo = function set_compress_level() { - Util.Error("Server sent compress level pseudo-encoding"); -}; + return (new DES(passwd)).encrypt(challenge); + }; -/* - * Client message routines - */ + RFB.extract_data_uri = function (arr) { + return ";base64," + Base64.encode(arr); + }; -pixelFormat = function() { - //Util.Debug(">> pixelFormat"); - var arr; - arr = [0]; // msg-type - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - - arr.push8(fb_Bpp * 8); // bits-per-pixel - arr.push8(fb_depth * 8); // depth - arr.push8(0); // little-endian - arr.push8(conf.true_color ? 1 : 0); // true-color - - arr.push16(255); // red-max - arr.push16(255); // green-max - arr.push16(255); // blue-max - arr.push8(16); // red-shift - arr.push8(8); // green-shift - arr.push8(0); // blue-shift - - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - //Util.Debug("<< pixelFormat"); - return arr; -}; - -clientEncodings = function() { - //Util.Debug(">> clientEncodings"); - var arr, i, encList = []; - - for (i=0; i> fbUpdateRequest"); - if (typeof(x) === "undefined") { x = 0; } - if (typeof(y) === "undefined") { y = 0; } - if (typeof(xw) === "undefined") { xw = fb_width; } - if (typeof(yw) === "undefined") { yw = fb_height; } - var arr; - arr = [3]; // msg-type - arr.push8(incremental); - arr.push16(x); - arr.push16(y); - arr.push16(xw); - arr.push16(yw); - //Util.Debug("<< fbUpdateRequest"); - return arr; -}; - -// Based on clean/dirty areas, generate requests to send -fbUpdateRequests = function() { - var cleanDirty = display.getCleanDirtyReset(), - arr = [], i, cb, db; - - cb = cleanDirty.cleanBox; - if (cb.w > 0 && cb.h > 0) { - // Request incremental for clean box - arr = arr.concat(fbUpdateRequest(1, cb.x, cb.y, cb.w, cb.h)); - } - for (i = 0; i < cleanDirty.dirtyBoxes.length; i++) { - db = cleanDirty.dirtyBoxes[i]; - // Force all (non-incremental for dirty box - arr = arr.concat(fbUpdateRequest(0, db.x, db.y, db.w, db.h)); - } - return arr; -}; - - - -keyEvent = function(keysym, down) { - //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down); - var arr; - arr = [4]; // msg-type - arr.push8(down); - arr.push16(0); - arr.push32(keysym); - //Util.Debug("<< keyEvent"); - return arr; -}; - -pointerEvent = function(x, y) { - //Util.Debug(">> pointerEvent, x,y: " + x + "," + y + - // " , mask: " + mouse_buttonMask); - var arr; - arr = [5]; // msg-type - arr.push8(mouse_buttonMask); - arr.push16(x); - arr.push16(y); - //Util.Debug("<< pointerEvent"); - return arr; -}; - -clientCutText = function(text) { - //Util.Debug(">> clientCutText"); - var arr, i, n; - arr = [6]; // msg-type - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - arr.push32(text.length); - n = text.length; - for (i=0; i < n; i+=1) { - arr.push(text.charCodeAt(i)); - } - //Util.Debug("<< clientCutText:" + arr); - return arr; -}; - - - -// -// Public API interface functions -// - -that.connect = function(host, port, password, path) { - //Util.Debug(">> connect"); - - rfb_host = host; - rfb_port = port; - rfb_password = (password !== undefined) ? password : ""; - rfb_path = (path !== undefined) ? path : ""; - - if ((!rfb_host) || (!rfb_port)) { - return fail("Must set host and port"); - } - - updateState('connect'); - //Util.Debug("<< connect"); - -}; - -that.disconnect = function() { - //Util.Debug(">> disconnect"); - updateState('disconnect', 'Disconnecting'); - //Util.Debug("<< disconnect"); -}; - -that.sendPassword = function(passwd) { - rfb_password = passwd; - rfb_state = "Authentication"; - setTimeout(init_msg, 1); -}; - -that.sendCtrlAltDel = function() { - if (rfb_state !== "normal" || conf.view_only) { return false; } - Util.Info("Sending Ctrl-Alt-Del"); - var arr = []; - arr = arr.concat(keyEvent(0xFFE3, 1)); // Control - arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt - arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete - arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete - arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt - arr = arr.concat(keyEvent(0xFFE3, 0)); // Control - arr = arr.concat(fbUpdateRequests()); - ws.send(arr); -}; - -// Send a key press. If 'down' is not specified then send a down key -// followed by an up key. -that.sendKey = function(code, down) { - if (rfb_state !== "normal" || conf.view_only) { return false; } - var arr = []; - if (typeof down !== 'undefined') { - Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); - arr = arr.concat(keyEvent(code, down ? 1 : 0)); - } else { - Util.Info("Sending key code (down + up): " + code); - arr = arr.concat(keyEvent(code, 1)); - arr = arr.concat(keyEvent(code, 0)); - } - arr = arr.concat(fbUpdateRequests()); - ws.send(arr); -}; - -that.clipboardPasteFrom = function(text) { - if (rfb_state !== "normal") { return; } - //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "..."); - ws.send(clientCutText(text)); - //Util.Debug("<< clipboardPasteFrom"); -}; - -// Override internal functions for testing -that.testMode = function(override_send) { - test_mode = true; - that.recv_message = ws.testMode(override_send); - - checkEvents = function () { /* Stub Out */ }; - that.connect = function(host, port, password) { - rfb_host = host; - rfb_port = port; - rfb_password = password; - updateState('ProtocolVersion', "Starting VNC handshake"); - }; -}; + RFB.encodingHandlers = { + RAW: function () { + if (this._FBU.lines === 0) { + this._FBU.lines = this._FBU.height; + } + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line + if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } + var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); + var curr_height = Math.min(this._FBU.lines, + Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); + this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, + curr_height, this._sock.get_rQ(), + this._sock.get_rQi()); + this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); + this._FBU.lines -= curr_height; + + if (this._FBU.lines > 0) { + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } -return constructor(); // Return the public API interface + return true; + }, + + COPYRECT: function () { + this._FBU.bytes = 4; + if (this._sock.rQwait("COPYRECT", 4)) { return false; } + this._display.renderQ_push({ + 'type': 'copy', + 'old_x': this._sock.rQshift16(), + 'old_y': this._sock.rQshift16(), + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + }); + this._FBU.rects--; + this._FBU.bytes = 0; + return true; + }, + + RRE: function () { + var color; + if (this._FBU.subrects === 0) { + this._FBU.bytes = 4 + this._fb_Bpp; + if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.subrects = this._sock.rQshift32(); + color = this._sock.rQshiftBytes(this._fb_Bpp); // Background + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); + } + + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { + color = this._sock.rQshiftBytes(this._fb_Bpp); + var x = this._sock.rQshift16(); + var y = this._sock.rQshift16(); + var width = this._sock.rQshift16(); + var height = this._sock.rQshift16(); + this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); + this._FBU.subrects--; + } + + if (this._FBU.subrects > 0) { + var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); + this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } -} // End of RFB() + return true; + }, + + HEXTILE: function () { + var rQ = this._sock.get_rQ(); + var rQi = this._sock.get_rQi(); + + if (this._FBU.tiles === 0) { + this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); + this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); + this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; + this._FBU.tiles = this._FBU.total_tiles; + } + + while (this._FBU.tiles > 0) { + this._FBU.bytes = 1; + if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } + var subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + this._fail("Disconnected: illegal hextile subencoding " + subencoding); + return false; + } + + var subrects = 0; + var curr_tile = this._FBU.total_tiles - this._FBU.tiles; + var tile_x = curr_tile % this._FBU.tiles_x; + var tile_y = Math.floor(curr_tile / this._FBU.tiles_x); + var x = this._FBU.x + tile_x * 16; + var y = this._FBU.y + tile_y * 16; + var w = Math.min(16, (this._FBU.x + this._FBU.width) - x); + var h = Math.min(16, (this._FBU.y + this._FBU.height) - y); + + // Figure out how much we are expecting + if (subencoding & 0x01) { // Raw + this._FBU.bytes += w * h * this._fb_Bpp; + } else { + if (subencoding & 0x02) { // Background + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x04) { // Foreground + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x08) { // AnySubrects + this._FBU.bytes++; // Since we aren't shifting it off + if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } + subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek + if (subencoding & 0x10) { // SubrectsColoured + this._FBU.bytes += subrects * (this._fb_Bpp + 2); + } else { + this._FBU.bytes += subrects * 2; + } + } + } + + if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } + + // We know the encoding and have a whole tile + this._FBU.subencoding = rQ[rQi]; + rQi++; + if (this._FBU.subencoding === 0) { + if (this._FBU.lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Util.Debug(" Ignoring blank after RAW"); + } else { + this._display.fillRect(x, y, w, h, rQ, rQi); + rQi += this._FBU.bytes - 1; + } + } else if (this._FBU.subencoding & 0x01) { // Raw + this._display.blitImage(x, y, w, h, rQ, rQi); + rQi += this._FBU.bytes - 1; + } else { + if (this._FBU.subencoding & 0x02) { // Background + this._FBU.background = rQ.slice(rQi, rQi + this._fb_Bpp); + rQi += this._fb_Bpp; + } + if (this._FBU.subencoding & 0x04) { // Foreground + this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp); + rQi += this._fb_Bpp; + } + + this._display.startTile(x, y, w, h, this._FBU.background); + if (this._FBU.subencoding & 0x08) { // AnySubrects + subrects = rQ[rQi]; + rQi++; + + for (var s = 0; s < subrects; s++) { + var color; + if (this._FBU.subencoding & 0x10) { // SubrectsColoured + color = rQ.slice(rQi, rQi + this._fb_Bpp); + rQi += this._fb_Bpp; + } else { + color = this._FBU.foreground; + } + var xy = rQ[rQi]; + rQi++; + var sx = (xy >> 4); + var sy = (xy & 0x0f); + + var wh = rQ[rQi]; + rQi++; + var sw = (wh >> 4) + 1; + var sh = (wh & 0x0f) + 1; + + this._display.subTile(sx, sy, sw, sh, color); + } + } + this._display.finishTile(); + } + this._sock.set_rQi(rQi); + this._FBU.lastsubencoding = this._FBU.subencoding; + this._FBU.bytes = 0; + this._FBU.tiles--; + } + + if (this._FBU.tiles === 0) { + this._FBU.rects--; + } + + return true; + }, + + getTightCLength: function (arr) { + var header = 1, data = 0; + data += arr[0] & 0x7f; + if (arr[0] & 0x80) { + header++; + data += (arr[1] & 0x7f) << 7; + if (arr[1] & 0x80) { + header++; + data += arr[2] << 14; + } + } + return [header, data]; + }, + + display_tight: function (isTightPNG) { + if (this._fb_depth === 1) { + this._fail("Tight protocol handler only implements true color mode"); + } + + this._FBU.bytes = 1; // compression-control byte + if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } + + var checksum = function (data) { + var sum = 0; + for (var i = 0; i < data.length; i++) { + sum += data[i]; + if (sum > 65536) sum -= 65536; + } + return sum; + }; + + var resetStreams = 0; + var streamId = -1; + var decompress = function (data) { + for (var i = 0; i < 4; i++) { + if ((resetStreams >> i) & 1) { + this._FBU.zlibs[i].reset(); + Util.Info("Reset zlib stream " + i); + } + } + + var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + if (uncompressed.status !== 0) { + Util.Error("Invalid data in zlib stream"); + } + + return uncompressed.data; + }.bind(this); + + var indexedToRGB = function (data, numColors, palette, width, height) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + var dest = []; + var x, y, dp, sp; + if (numColors === 2) { + var w = Math.floor((width + 7) / 8); + var w1 = Math.floor(width / 8); + + for (y = 0; y < height; y++) { + var b; + for (x = 0; x < w1; x++) { + for (b = 7; b >= 0; b--) { + dp = (y * width + x * 8 + 7 - b) * 3; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + + for (b = 7; b >= 8 - width % 8; b--) { + dp = (y * width + x * 8 + 7 - b) * 3; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + } else { + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + dp = (y * width + x) * 3; + sp = data[y * width + x] * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + } + + return dest; + }.bind(this); + + var rQ = this._sock.get_rQ(); + var rQi = this._sock.get_rQi(); + var cmode, clength, data; + + var handlePalette = function () { + var numColors = rQ[rQi + 2] + 1; + var paletteSize = numColors * this._fb_depth; + this._FBU.bytes += paletteSize; + if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } + + var bpp = (numColors <= 2) ? 1 : 8; + var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); + var raw = false; + if (rowSize * this._FBU.height < 12) { + raw = true; + clength = [0, rowSize * this._FBU.height]; + } else { + clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(3 + paletteSize, + 3 + paletteSize + 3)); + } + + this._FBU.bytes += clength[0] + clength[1]; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, filter id, num colors, palette entries, and clength off + this._sock.rQskipBytes(3); + var palette = this._sock.rQshiftBytes(paletteSize); + this._sock.rQskipBytes(clength[0]); + + if (raw) { + data = this._sock.rQshiftBytes(clength[1]); + } else { + data = decompress(this._sock.rQshiftBytes(clength[1])); + } + + // Convert indexed (palette based) image data to RGB + var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height); + + this._display.renderQ_push({ + 'type': 'blitRgb', + 'data': rgb, + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + }); + + return true; + }.bind(this); + + var handleCopy = function () { + var raw = false; + var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + if (uncompressedSize < 12) { + raw = true; + clength = [0, uncompressedSize]; + } else { + clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); + } + this._FBU.bytes = 1 + clength[0] + clength[1]; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, clength off + this._sock.rQshiftBytes(1 + clength[0]); + + if (raw) { + data = this._sock.rQshiftBytes(clength[1]); + } else { + data = decompress(this._sock.rQshiftBytes(clength[1])); + } + + this._display.renderQ_push({ + 'type': 'blitRgb', + 'data': data, + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + }); + + return true; + }.bind(this); + + var ctl = this._sock.rQpeek8(); + + // Keep tight reset bits + resetStreams = ctl & 0xF; + + // Figure out filter + ctl = ctl >> 4; + streamId = ctl & 0x3; + + if (ctl === 0x08) cmode = "fill"; + else if (ctl === 0x09) cmode = "jpeg"; + else if (ctl === 0x0A) cmode = "png"; + else if (ctl & 0x04) cmode = "filter"; + else if (ctl < 0x04) cmode = "copy"; + else return this._fail("Illegal tight compression received, ctl: " + ctl); + + if (isTightPNG && (cmode === "filter" || cmode === "copy")) { + return this._fail("filter/copy received in tightPNG mode"); + } + + switch (cmode) { + // fill use fb_depth because TPIXELs drop the padding byte + case "fill": // TPIXEL + this._FBU.bytes += this._fb_depth; + break; + case "jpeg": // max clength + this._FBU.bytes += 3; + break; + case "png": // max clength + this._FBU.bytes += 3; + break; + case "filter": // filter id + num colors if palette + this._FBU.bytes += 2; + break; + case "copy": + break; + } + + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Determine FBU.bytes + switch (cmode) { + case "fill": + this._sock.rQskip8(); // shift off ctl + var color = this._sock.rQshiftBytes(this._fb_depth); + this._display.renderQ_push({ + 'type': 'fill', + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height, + 'color': [color[2], color[1], color[0]] + }); + break; + case "png": + case "jpeg": + clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); + this._FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // We have everything, render it + this._sock.rQskipBytes(1 + clength[0]); // shift off clt + compact length + var img = new Image(); + img.src = "data: image/" + cmode + + RFB.extract_data_uri(this._sock.rQshiftBytes(clength[1])); + this._display.renderQ_push({ + 'type': 'img', + 'img': img, + 'x': this._FBU.x, + 'y': this._FBU.y + }); + img = null; + break; + case "filter": + var filterId = rQ[rQi + 1]; + if (filterId === 1) { + if (!handlePalette()) { return false; } + } else { + // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter + // Filter 2, Gradient is valid but not use if jpeg is enabled + // TODO(directxman12): why aren't we just calling '_fail' here + throw new Error("Unsupported tight subencoding received, filter: " + filterId); + } + break; + case "copy": + if (!handleCopy()) { return false; } + break; + } + + + this._FBU.bytes = 0; + this._FBU.rects--; + + return true; + }, + + TIGHT: function () { return this._encHandlers.display_tight(false); }, + TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, + + last_rect: function () { + this._FBU.rects = 0; + return true; + }, + + DesktopSize: function () { + Util.Debug(">> set_desktopsize"); + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._onFBResize(this, this._fb_width, this._fb_height); + this._display.resize(this._fb_width, this._fb_height); + this._timing.fbu_rt_start = (new Date()).getTime(); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Util.Debug("<< set_desktopsize"); + return true; + }, + + Cursor: function () { + Util.Debug(">> set_cursor"); + var x = this._FBU.x; // hotspot-x + var y = this._FBU.y; // hotspot-y + var w = this._FBU.width; + var h = this._FBU.height; + + var pixelslength = w * h * this._fb_Bpp; + var masklength = Math.floor((w + 7) / 8) * h; + + this._FBU.bytes = pixelslength + masklength; + if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } + + this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), + this._sock.rQshiftBytes(masklength), + x, y, w, h); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Util.Debug("<< set_cursor"); + return true; + }, + + JPEG_quality_lo: function () { + Util.Error("Server sent jpeg_quality pseudo-encoding"); + }, + + compress_lo: function () { + Util.Error("Server sent compress level pseudo-encoding"); + } + }; +})(); diff --git a/webclients/novnc/include/ui.js b/webclients/novnc/include/ui.js index dc837e6..e869aa6 100644 --- a/webclients/novnc/include/ui.js +++ b/webclients/novnc/include/ui.js @@ -1,659 +1,979 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin - * Licensed under LGPL-3 (see LICENSE.txt) + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. */ -"use strict"; -/*jslint white: false, browser: true */ -/*global window, $D, Util, WebUtil, RFB, Display */ - -var UI = { - -rfb_state : 'loaded', -settingsOpen : false, -connSettingsOpen : false, -clipboardOpen: false, -keyboardVisible: false, - -// Render default UI and initialize settings menu -load: function() { - var html = '', i, sheet, sheets, llevels; - - // Stylesheet selection dropdown - sheet = WebUtil.selectStylesheet(); - sheets = WebUtil.getStylesheets(); - for (i = 0; i < sheets.length; i += 1) { - UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); - } - - // Logging selection dropdown - llevels = ['error', 'warn', 'info', 'debug']; - for (i = 0; i < llevels.length; i += 1) { - UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); - } - - // Settings with immediate effects - UI.initSetting('logging', 'warn'); - WebUtil.init_logging(UI.getSetting('logging')); - - UI.initSetting('stylesheet', 'default'); - WebUtil.selectStylesheet(null); - // call twice to get around webkit bug - WebUtil.selectStylesheet(UI.getSetting('stylesheet')); - - /* Populate the controls if defaults are provided in the URL */ - UI.initSetting('host', window.location.hostname); - UI.initSetting('port', window.location.port); - UI.initSetting('password', ''); - UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('true_color', true); - UI.initSetting('cursor', false); - UI.initSetting('shared', true); - UI.initSetting('view_only', false); - UI.initSetting('connectTimeout', 2); - UI.initSetting('path', 'websockify'); - UI.initSetting('repeaterID', ''); - - UI.rfb = RFB({'target': $D('noVNC_canvas'), - 'onUpdateState': UI.updateState, - 'onClipboard': UI.clipReceive}); - UI.updateVisualState(); - - // Unfocus clipboard when over the VNC area - //$D('VNC_screen').onmousemove = function () { - // var keyboard = UI.rfb.get_keyboard(); - // if ((! keyboard) || (! keyboard.get_focused())) { - // $D('VNC_clipboard_text').blur(); - // } - // }; - - // Show mouse selector buttons on touch screen devices - if ('ontouchstart' in document.documentElement) { - // Show mobile buttons - $D('noVNC_mobile_buttons').style.display = "inline"; - UI.setMouseButton(); - // Remove the address bar - setTimeout(function() { window.scrollTo(0, 1); }, 100); - UI.forceSetting('clip', true); - $D('noVNC_clip').disabled = true; - } else { - UI.initSetting('clip', false); - } - - //iOS Safari does not support CSS position:fixed. - //This detects iOS devices and enables javascript workaround. - if ((navigator.userAgent.match(/iPhone/i)) || - (navigator.userAgent.match(/iPod/i)) || - (navigator.userAgent.match(/iPad/i))) { - //UI.setOnscroll(); - //UI.setResize(); - } - - $D('noVNC_host').focus(); - - UI.setViewClip(); - Util.addEvent(window, 'resize', UI.setViewClip); - - Util.addEvent(window, 'beforeunload', function () { - if (UI.rfb_state === 'normal') { - return "You are currently connected."; - } - } ); - - // Show description by default when hosted at for kanaka.github.com - if (location.host === "kanaka.github.com") { - // Open the description dialog - $D('noVNC_description').style.display = "block"; - } else { - // Open the connect panel on first load - UI.toggleConnectPanel(); - } -}, - -// Read form control compatible setting from cookie -getSetting: function(name) { - var val, ctrl = $D('noVNC_' + name); - val = WebUtil.readCookie(name); - if (ctrl.type === 'checkbox') { - if (val.toLowerCase() in {'0':1, 'no':1, 'false':1}) { - val = false; - } else { - val = true; - } - } - return val; -}, +/* jslint white: false, browser: true */ +/* global window, $D, Util, WebUtil, RFB, Display */ + +var UI; + +(function () { + "use strict"; + + // Load supporting scripts + window.onscriptsload = function () { UI.load(); }; + window.onload = function () { UI.keyboardinputReset(); }; + Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", + "keysymdef.js", "keyboard.js", "input.js", "display.js", + "jsunzip.js", "rfb.js", "keysym.js"]); + + var UI = { + + rfb_state : 'loaded', + settingsOpen : false, + connSettingsOpen : false, + popupStatusOpen : false, + clipboardOpen: false, + keyboardVisible: false, + hideKeyboardTimeout: null, + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, + extraKeysVisible: false, + ctrlOn: false, + altOn: false, + isTouchDevice: false, + + // Setup rfb object, load settings from browser storage, then call + // UI.init to setup the UI/menus + load: function (callback) { + WebUtil.initSettings(UI.start, callback); + }, + + // Render default UI and initialize settings menu + start: function(callback) { + UI.isTouchDevice = 'ontouchstart' in document.documentElement; + + // Stylesheet selection dropdown + var sheet = WebUtil.selectStylesheet(); + var sheets = WebUtil.getStylesheets(); + var i; + for (i = 0; i < sheets.length; i += 1) { + UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); + } -// Update cookie and form control setting. If value is not set, then -// updates from control to current cookie setting. -updateSetting: function(name, value) { + // Logging selection dropdown + var llevels = ['error', 'warn', 'info', 'debug']; + for (i = 0; i < llevels.length; i += 1) { + UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); + } - var i, ctrl = $D('noVNC_' + name); - // Save the cookie for this session - if (typeof value !== 'undefined') { - WebUtil.createCookie(name, value); - } + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + WebUtil.init_logging(UI.getSetting('logging')); + + UI.initSetting('stylesheet', 'default'); + WebUtil.selectStylesheet(null); + // call twice to get around webkit bug + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + + // if port == 80 (or 443) then it won't be present and should be + // set manually + var port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0,5) == 'https') { + port = 443; + } + else if (window.location.protocol.substring(0,4) == 'http') { + port = 80; + } + } - // Update the settings control - value = UI.getSetting(name); + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('password', ''); + UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('true_color', true); + UI.initSetting('cursor', !UI.isTouchDevice); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + + UI.rfb = new RFB({'target': $D('noVNC_canvas'), + 'onUpdateState': UI.updateState, + 'onXvpInit': UI.updateXvpVisualState, + 'onClipboard': UI.clipReceive, + 'onDesktopName': UI.updateDocumentTitle}); + + var autoconnect = WebUtil.getQueryVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + } - if (ctrl.type === 'checkbox') { - ctrl.checked = value; + UI.updateVisualState(); + + // Show mouse selector buttons on touch screen devices + if (UI.isTouchDevice) { + // Show mobile buttons + $D('noVNC_mobile_buttons').style.display = "inline"; + UI.setMouseButton(); + // Remove the address bar + setTimeout(function() { window.scrollTo(0, 1); }, 100); + UI.forceSetting('clip', true); + $D('noVNC_clip').disabled = true; + } else { + UI.initSetting('clip', false); + } - } else if (typeof ctrl.options !== 'undefined') { - for (i = 0; i < ctrl.options.length; i += 1) { - if (ctrl.options[i].value === value) { - ctrl.selectedIndex = i; - break; + //iOS Safari does not support CSS position:fixed. + //This detects iOS devices and enables javascript workaround. + if ((navigator.userAgent.match(/iPhone/i)) || + (navigator.userAgent.match(/iPod/i)) || + (navigator.userAgent.match(/iPad/i))) { + //UI.setOnscroll(); + //UI.setResize(); } - } - } else { - /*Weird IE9 error leads to 'null' appearring - in textboxes instead of ''.*/ - if (value === null) { - value = ""; - } - ctrl.value = value; - } -}, - -// Save control setting to cookie -saveSetting: function(name) { - var val, ctrl = $D('noVNC_' + name); - if (ctrl.type === 'checkbox') { - val = ctrl.checked; - } else if (typeof ctrl.options !== 'undefined') { - val = ctrl.options[ctrl.selectedIndex].value; - } else { - val = ctrl.value; - } - WebUtil.createCookie(name, val); - //Util.Debug("Setting saved '" + name + "=" + val + "'"); - return val; -}, - -// Initial page load read/initialization of settings -initSetting: function(name, defVal) { - var val; - - // Check Query string followed by cookie - val = WebUtil.getQueryVar(name); - if (val === null) { - val = WebUtil.readCookie(name, defVal); - } - UI.updateSetting(name, val); - //Util.Debug("Setting '" + name + "' initialized to '" + val + "'"); - return val; -}, - -// Force a setting to be a certain value -forceSetting: function(name, val) { - UI.updateSetting(name, val); - return val; -}, - - -// Show the clipboard panel -toggleClipboardPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - //Close settings if open - if (UI.settingsOpen === true) { - UI.settingsApply(); - UI.closeSettingsMenu(); - } - //Close connection settings if open - if (UI.connSettingsOpen === true) { - UI.toggleConnectPanel(); - } - //Toggle Clipboard Panel - if (UI.clipboardOpen === true) { - $D('noVNC_clipboard').style.display = "none"; - $D('clipboardButton').className = "noVNC_status_button"; - UI.clipboardOpen = false; - } else { - $D('noVNC_clipboard').style.display = "block"; - $D('clipboardButton').className = "noVNC_status_button_selected"; - UI.clipboardOpen = true; - } -}, - -// Show the connection settings panel/menu -toggleConnectPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - //Close connection settings if open - if (UI.settingsOpen === true) { - UI.settingsApply(); - UI.closeSettingsMenu(); - $D('connectButton').className = "noVNC_status_button"; - } - if (UI.clipboardOpen === true) { - UI.toggleClipboardPanel(); - } - - //Toggle Connection Panel - if (UI.connSettingsOpen === true) { - $D('noVNC_controls').style.display = "none"; - $D('connectButton').className = "noVNC_status_button"; - UI.connSettingsOpen = false; - } else { - $D('noVNC_controls').style.display = "block"; - $D('connectButton').className = "noVNC_status_button_selected"; - UI.connSettingsOpen = true; - $D('noVNC_host').focus(); - } -}, - -// Toggle the settings menu: -// On open, settings are refreshed from saved cookies. -// On close, settings are applied -toggleSettingsPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - if (UI.settingsOpen) { - UI.settingsApply(); - UI.closeSettingsMenu(); - } else { - UI.updateSetting('encrypt'); - UI.updateSetting('true_color'); - if (UI.rfb.get_display().get_cursor_uri()) { - UI.updateSetting('cursor'); - } else { - UI.updateSetting('cursor', false); - $D('noVNC_cursor').disabled = true; - } - UI.updateSetting('clip'); - UI.updateSetting('shared'); - UI.updateSetting('view_only'); - UI.updateSetting('connectTimeout'); - UI.updateSetting('path'); - UI.updateSetting('repeaterID'); - UI.updateSetting('stylesheet'); - UI.updateSetting('logging'); - - UI.openSettingsMenu(); - } -}, - -// Open menu -openSettingsMenu: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - if (UI.clipboardOpen === true) { - UI.toggleClipboardPanel(); - } - //Close connection settings if open - if (UI.connSettingsOpen === true) { - UI.toggleConnectPanel(); - } - $D('noVNC_settings').style.display = "block"; - $D('settingsButton').className = "noVNC_status_button_selected"; - UI.settingsOpen = true; -}, - -// Close menu (without applying settings) -closeSettingsMenu: function() { - $D('noVNC_settings').style.display = "none"; - $D('settingsButton').className = "noVNC_status_button"; - UI.settingsOpen = false; -}, - -// Save/apply settings when 'Apply' button is pressed -settingsApply: function() { - //Util.Debug(">> settingsApply"); - UI.saveSetting('encrypt'); - UI.saveSetting('true_color'); - if (UI.rfb.get_display().get_cursor_uri()) { - UI.saveSetting('cursor'); - } - UI.saveSetting('clip'); - UI.saveSetting('shared'); - UI.saveSetting('view_only'); - UI.saveSetting('connectTimeout'); - UI.saveSetting('path'); - UI.saveSetting('repeaterID'); - UI.saveSetting('stylesheet'); - UI.saveSetting('logging'); - - // Settings with immediate (non-connected related) effect - WebUtil.selectStylesheet(UI.getSetting('stylesheet')); - WebUtil.init_logging(UI.getSetting('logging')); - UI.setViewClip(); - UI.setViewDrag(UI.rfb.get_viewportDrag()); - //Util.Debug("<< settingsApply"); -}, - - - -setPassword: function() { - UI.rfb.sendPassword($D('noVNC_password').value); - //Reset connect button. - $D('noVNC_connect_button').value = "Connect"; - $D('noVNC_connect_button').onclick = UI.Connect; - //Hide connection panel. - UI.toggleConnectPanel(); - return false; -}, - -sendCtrlAltDel: function() { - UI.rfb.sendCtrlAltDel(); -}, - -setMouseButton: function(num) { - var b, blist = [0, 1,2,4], button; - - if (typeof num === 'undefined') { - // Disable mouse buttons - num = -1; - } - if (UI.rfb) { - UI.rfb.get_mouse().set_touchButton(num); - } - - for (b = 0; b < blist.length; b++) { - button = $D('noVNC_mouse_button' + blist[b]); - if (blist[b] === num) { - button.style.display = ""; - } else { - button.style.display = "none"; - /* - button.style.backgroundColor = "black"; - button.style.color = "lightgray"; - button.style.backgroundColor = ""; - button.style.color = ""; - */ - } - } -}, - -updateState: function(rfb, state, oldstate, msg) { - var s, sb, c, d, cad, vd, klass; - UI.rfb_state = state; - s = $D('noVNC_status'); - sb = $D('noVNC_status_bar'); - switch (state) { - case 'failed': - case 'fatal': - klass = "noVNC_status_error"; - break; - case 'normal': - klass = "noVNC_status_normal"; - break; - case 'disconnected': - $D('noVNC_logo').style.display = "block"; - // Fall through - case 'loaded': - klass = "noVNC_status_normal"; - break; - case 'password': + UI.setBarPosition(); + + $D('noVNC_host').focus(); + + UI.setViewClip(); + Util.addEvent(window, 'resize', UI.setViewClip); + + Util.addEvent(window, 'beforeunload', function () { + if (UI.rfb_state === 'normal') { + return "You are currently connected."; + } + } ); + + // Show description by default when hosted at for kanaka.github.com + if (location.host === "kanaka.github.io") { + // Open the description dialog + $D('noVNC_description').style.display = "block"; + } else { + // Show the connect panel on first load unless autoconnecting + if (autoconnect === UI.connSettingsOpen) { + UI.toggleConnectPanel(); + } + } + + // Add mouse event click/focus/blur event handlers to the UI + UI.addMouseHandlers(); + + if (typeof callback === "function") { + callback(UI.rfb); + } + }, + + addMouseHandlers: function() { + // Setup interface handlers that can't be inline + $D("noVNC_view_drag_button").onclick = UI.setViewDrag; + $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; + $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; + $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; + $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); }; + $D("showKeyboard").onclick = UI.showKeyboard; + + $D("keyboardinput").oninput = UI.keyInput; + $D("keyboardinput").onblur = UI.keyInputBlur; + + $D("showExtraKeysButton").onclick = UI.showExtraKeys; + $D("toggleCtrlButton").onclick = UI.toggleCtrl; + $D("toggleAltButton").onclick = UI.toggleAlt; + $D("sendTabButton").onclick = UI.sendTab; + $D("sendEscButton").onclick = UI.sendEsc; + + $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel; + $D("xvpShutdownButton").onclick = UI.xvpShutdown; + $D("xvpRebootButton").onclick = UI.xvpReboot; + $D("xvpResetButton").onclick = UI.xvpReset; + $D("noVNC_status").onclick = UI.togglePopupStatusPanel; + $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel; + $D("xvpButton").onclick = UI.toggleXvpPanel; + $D("clipboardButton").onclick = UI.toggleClipboardPanel; + $D("settingsButton").onclick = UI.toggleSettingsPanel; + $D("connectButton").onclick = UI.toggleConnectPanel; + $D("disconnectButton").onclick = UI.disconnect; + $D("descriptionButton").onclick = UI.toggleConnectPanel; + + $D("noVNC_clipboard_text").onfocus = UI.displayBlur; + $D("noVNC_clipboard_text").onblur = UI.displayFocus; + $D("noVNC_clipboard_text").onchange = UI.clipSend; + $D("noVNC_clipboard_clear_button").onclick = UI.clipClear; + + $D("noVNC_settings_menu").onmouseover = UI.displayBlur; + $D("noVNC_settings_menu").onmouseover = UI.displayFocus; + $D("noVNC_apply").onclick = UI.settingsApply; + + $D("noVNC_connect_button").onclick = UI.connect; + }, + + // Read form control compatible setting from cookie + getSetting: function(name) { + var ctrl = $D('noVNC_' + name); + var val = WebUtil.readSetting(name); + if (val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { + val = false; + } else { + val = true; + } + } + return val; + }, + + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting: function(name, value) { + + // Save the cookie for this session + if (typeof value !== 'undefined') { + WebUtil.writeSetting(name, value); + } + + // Update the settings control + value = UI.getSetting(name); + + var ctrl = $D('noVNC_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + + } else if (typeof ctrl.options !== 'undefined') { + for (var i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; + } + } + } else { + /*Weird IE9 error leads to 'null' appearring + in textboxes instead of ''.*/ + if (value === null) { + value = ""; + } + ctrl.value = value; + } + }, + + // Save control setting to cookie + saveSetting: function(name) { + var val, ctrl = $D('noVNC_' + name); + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Util.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, + + // Initial page load read/initialization of settings + initSetting: function(name, defVal) { + // Check Query string followed by cookie + var val = WebUtil.getQueryVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + UI.updateSetting(name, val); + return val; + }, + + // Force a setting to be a certain value + forceSetting: function(name, val) { + UI.updateSetting(name, val); + return val; + }, + + + // Show the popup status panel + togglePopupStatusPanel: function() { + var psp = $D('noVNC_popup_status_panel'); + if (UI.popupStatusOpen === true) { + psp.style.display = "none"; + UI.popupStatusOpen = false; + } else { + psp.innerHTML = $D('noVNC_status').innerHTML; + psp.style.display = "block"; + psp.style.left = window.innerWidth/2 - + parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px"; + UI.popupStatusOpen = true; + } + }, + + // Show the XVP panel + toggleXvpPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Toggle XVP panel + if (UI.xvpOpen === true) { + $D('noVNC_xvp').style.display = "none"; + $D('xvpButton').className = "noVNC_status_button"; + UI.xvpOpen = false; + } else { + $D('noVNC_xvp').style.display = "block"; + $D('xvpButton').className = "noVNC_status_button_selected"; + UI.xvpOpen = true; + } + }, + + // Show the clipboard panel + toggleClipboardPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + // Toggle Clipboard Panel + if (UI.clipboardOpen === true) { + $D('noVNC_clipboard').style.display = "none"; + $D('clipboardButton').className = "noVNC_status_button"; + UI.clipboardOpen = false; + } else { + $D('noVNC_clipboard').style.display = "block"; + $D('clipboardButton').className = "noVNC_status_button_selected"; + UI.clipboardOpen = true; + } + }, + + // Show the connection settings panel/menu + toggleConnectPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close connection settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + $D('connectButton').className = "noVNC_status_button"; + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + + // Toggle Connection Panel + if (UI.connSettingsOpen === true) { + $D('noVNC_controls').style.display = "none"; + $D('connectButton').className = "noVNC_status_button"; + UI.connSettingsOpen = false; + UI.saveSetting('host'); + UI.saveSetting('port'); + //UI.saveSetting('password'); + } else { + $D('noVNC_controls').style.display = "block"; + $D('connectButton').className = "noVNC_status_button_selected"; + UI.connSettingsOpen = true; + $D('noVNC_host').focus(); + } + }, + + // Toggle the settings menu: + // On open, settings are refreshed from saved cookies. + // On close, settings are applied + toggleSettingsPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + if (UI.settingsOpen) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } else { + UI.updateSetting('encrypt'); + UI.updateSetting('true_color'); + if (UI.rfb.get_display().get_cursor_uri()) { + UI.updateSetting('cursor'); + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + UI.updateSetting('clip'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('stylesheet'); + UI.updateSetting('logging'); + + UI.openSettingsMenu(); + } + }, + + // Open menu + openSettingsMenu: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + $D('noVNC_settings').style.display = "block"; + $D('settingsButton').className = "noVNC_status_button_selected"; + UI.settingsOpen = true; + }, + + // Close menu (without applying settings) + closeSettingsMenu: function() { + $D('noVNC_settings').style.display = "none"; + $D('settingsButton').className = "noVNC_status_button"; + UI.settingsOpen = false; + }, + + // Save/apply settings when 'Apply' button is pressed + settingsApply: function() { + //Util.Debug(">> settingsApply"); + UI.saveSetting('encrypt'); + UI.saveSetting('true_color'); + if (UI.rfb.get_display().get_cursor_uri()) { + UI.saveSetting('cursor'); + } + UI.saveSetting('clip'); + UI.saveSetting('shared'); + UI.saveSetting('view_only'); + UI.saveSetting('path'); + UI.saveSetting('repeaterID'); + UI.saveSetting('stylesheet'); + UI.saveSetting('logging'); + + // Settings with immediate (non-connected related) effect + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + WebUtil.init_logging(UI.getSetting('logging')); + UI.setViewClip(); + UI.setViewDrag(UI.rfb.get_viewportDrag()); + //Util.Debug("<< settingsApply"); + }, + + + + setPassword: function() { + UI.rfb.sendPassword($D('noVNC_password').value); + //Reset connect button. + $D('noVNC_connect_button').value = "Connect"; + $D('noVNC_connect_button').onclick = UI.Connect; + //Hide connection panel. UI.toggleConnectPanel(); + return false; + }, - $D('noVNC_connect_button').value = "Send Password"; - $D('noVNC_connect_button').onclick = UI.setPassword; - $D('noVNC_password').focus(); - - klass = "noVNC_status_warn"; - break; - default: - klass = "noVNC_status_warn"; - break; - } - - if (typeof(msg) !== 'undefined') { - s.setAttribute("class", klass); - sb.setAttribute("class", klass); - s.innerHTML = msg; - } - - UI.updateVisualState(); -}, - -// Disable/enable controls depending on connection state -updateVisualState: function() { - var connected = UI.rfb_state === 'normal' ? true : false; - - //Util.Debug(">> updateVisualState"); - $D('noVNC_encrypt').disabled = connected; - $D('noVNC_true_color').disabled = connected; - if (UI.rfb && UI.rfb.get_display() && - UI.rfb.get_display().get_cursor_uri()) { - $D('noVNC_cursor').disabled = connected; - } else { - UI.updateSetting('cursor', false); - $D('noVNC_cursor').disabled = true; - } - $D('noVNC_shared').disabled = connected; - $D('noVNC_view_only').disabled = connected; - $D('noVNC_connectTimeout').disabled = connected; - $D('noVNC_path').disabled = connected; - $D('noVNC_repeaterID').disabled = connected; - - if (connected) { - UI.setViewClip(); - UI.setMouseButton(1); - $D('clipboardButton').style.display = "inline"; - $D('showKeyboard').style.display = "inline"; - $D('sendCtrlAltDelButton').style.display = "inline"; - } else { - UI.setMouseButton(); - $D('clipboardButton').style.display = "none"; - $D('showKeyboard').style.display = "none"; - $D('sendCtrlAltDelButton').style.display = "none"; - } - // State change disables viewport dragging. - // It is enabled (toggled) by direct click on the button - UI.setViewDrag(false); - - switch (UI.rfb_state) { - case 'fatal': - case 'failed': - case 'loaded': - case 'disconnected': - $D('connectButton').style.display = ""; - $D('disconnectButton').style.display = "none"; - break; - default: - $D('connectButton').style.display = "none"; - $D('disconnectButton').style.display = ""; - break; - } - - //Util.Debug("<< updateVisualState"); -}, - - -clipReceive: function(rfb, text) { - Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); - $D('noVNC_clipboard_text').value = text; - Util.Debug("<< UI.clipReceive"); -}, - - -connect: function() { - var host, port, password, path; - - UI.closeSettingsMenu(); - UI.toggleConnectPanel(); - - host = $D('noVNC_host').value; - port = $D('noVNC_port').value; - password = $D('noVNC_password').value; - path = $D('noVNC_path').value; - if ((!host) || (!port)) { - throw("Must set host and port"); - } - - UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_true_color(UI.getSetting('true_color')); - UI.rfb.set_local_cursor(UI.getSetting('cursor')); - UI.rfb.set_shared(UI.getSetting('shared')); - UI.rfb.set_view_only(UI.getSetting('view_only')); - UI.rfb.set_connectTimeout(UI.getSetting('connectTimeout')); - UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); - - UI.rfb.connect(host, port, password, path); - - //Close dialog. - setTimeout(UI.setBarPosition, 100); - $D('noVNC_logo').style.display = "none"; -}, - -disconnect: function() { - UI.closeSettingsMenu(); - UI.rfb.disconnect(); - - $D('noVNC_logo').style.display = "block"; - UI.connSettingsOpen = false; - UI.toggleConnectPanel(); -}, - -displayBlur: function() { - UI.rfb.get_keyboard().set_focused(false); - UI.rfb.get_mouse().set_focused(false); -}, - -displayFocus: function() { - UI.rfb.get_keyboard().set_focused(true); - UI.rfb.get_mouse().set_focused(true); -}, - -clipClear: function() { - $D('noVNC_clipboard_text').value = ""; - UI.rfb.clipboardPasteFrom(""); -}, - -clipSend: function() { - var text = $D('noVNC_clipboard_text').value; - Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); - UI.rfb.clipboardPasteFrom(text); - Util.Debug("<< UI.clipSend"); -}, - - -// Enable/disable and configure viewport clipping -setViewClip: function(clip) { - var display, cur_clip, pos, new_w, new_h; - - if (UI.rfb) { - display = UI.rfb.get_display(); - } else { - return; - } - - cur_clip = display.get_viewport(); - - if (typeof(clip) !== 'boolean') { - // Use current setting - clip = UI.getSetting('clip'); - } - - if (clip && !cur_clip) { - // Turn clipping on - UI.updateSetting('clip', true); - } else if (!clip && cur_clip) { - // Turn clipping off - UI.updateSetting('clip', false); - display.set_viewport(false); - $D('noVNC_canvas').style.position = 'static'; - display.viewportChange(); - } - if (UI.getSetting('clip')) { - // If clipping, update clipping settings - $D('noVNC_canvas').style.position = 'absolute'; - pos = Util.getPosition($D('noVNC_canvas')); - new_w = window.innerWidth - pos.x; - new_h = window.innerHeight - pos.y; - display.set_viewport(true); - display.viewportChange(0, 0, new_w, new_h); - } -}, - -// Toggle/set/unset the viewport drag/move button -setViewDrag: function(drag) { - var vmb = $D('noVNC_view_drag_button'); - if (!UI.rfb) { return; } - - if (UI.rfb_state === 'normal' && - UI.rfb.get_display().get_viewport()) { - vmb.style.display = "inline"; - } else { - vmb.style.display = "none"; - } - - if (typeof(drag) === "undefined") { - // If not specified, then toggle - drag = !UI.rfb.get_viewportDrag(); - } - if (drag) { - vmb.className = "noVNC_status_button_selected"; - UI.rfb.set_viewportDrag(true); - } else { - vmb.className = "noVNC_status_button"; - UI.rfb.set_viewportDrag(false); - } -}, - -// On touch devices, show the OS keyboard -showKeyboard: function() { - if(UI.keyboardVisible === false) { - $D('keyboardinput').focus(); - UI.keyboardVisible = true; - $D('showKeyboard').className = "noVNC_status_button_selected"; - } else if(UI.keyboardVisible === true) { - $D('keyboardinput').blur(); - $D('showKeyboard').className = "noVNC_status_button"; - UI.keyboardVisible = false; - } -}, - -keyInputBlur: function() { - $D('showKeyboard').className = "noVNC_status_button"; - //Weird bug in iOS if you change keyboardVisible - //here it does not actually occur so next time - //you click keyboard icon it doesnt work. - setTimeout(function() { UI.setKeyboard(); },100); -}, - -setKeyboard: function() { - UI.keyboardVisible = false; -}, - -// iOS < Version 5 does not support position fixed. Javascript workaround: -setOnscroll: function() { - window.onscroll = function() { - UI.setBarPosition(); - }; -}, + sendCtrlAltDel: function() { + UI.rfb.sendCtrlAltDel(); + }, -setResize: function () { - window.onResize = function() { - UI.setBarPosition(); - }; -}, + xvpShutdown: function() { + UI.rfb.xvpShutdown(); + }, -//Helper to add options to dropdown. -addOption: function(selectbox,text,value ) -{ - var optn = document.createElement("OPTION"); - optn.text = text; - optn.value = value; - selectbox.options.add(optn); -}, + xvpReboot: function() { + UI.rfb.xvpReboot(); + }, -setBarPosition: function() { - $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; - $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; + xvpReset: function() { + UI.rfb.xvpReset(); + }, - var vncwidth = $D('noVNC_screen').style.offsetWidth; - $D('noVNC-control-bar').style.width = vncwidth + 'px'; -} + setMouseButton: function(num) { + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; + } + if (UI.rfb) { + UI.rfb.get_mouse().set_touchButton(num); + } -}; + var blist = [0, 1,2,4]; + for (var b = 0; b < blist.length; b++) { + var button = $D('noVNC_mouse_button' + blist[b]); + if (blist[b] === num) { + button.style.display = ""; + } else { + button.style.display = "none"; + } + } + }, + + updateState: function(rfb, state, oldstate, msg) { + UI.rfb_state = state; + var klass; + switch (state) { + case 'failed': + case 'fatal': + klass = "noVNC_status_error"; + break; + case 'normal': + klass = "noVNC_status_normal"; + break; + case 'disconnected': + $D('noVNC_logo').style.display = "block"; + /* falls through */ + case 'loaded': + klass = "noVNC_status_normal"; + break; + case 'password': + UI.toggleConnectPanel(); + + $D('noVNC_connect_button').value = "Send Password"; + $D('noVNC_connect_button').onclick = UI.setPassword; + $D('noVNC_password').focus(); + + klass = "noVNC_status_warn"; + break; + default: + klass = "noVNC_status_warn"; + break; + } + if (typeof(msg) !== 'undefined') { + $D('noVNC-control-bar').setAttribute("class", klass); + $D('noVNC_status').innerHTML = msg; + } + UI.updateVisualState(); + }, + + // Disable/enable controls depending on connection state + updateVisualState: function() { + var connected = UI.rfb_state === 'normal' ? true : false; + + //Util.Debug(">> updateVisualState"); + $D('noVNC_encrypt').disabled = connected; + $D('noVNC_true_color').disabled = connected; + if (UI.rfb && UI.rfb.get_display() && + UI.rfb.get_display().get_cursor_uri()) { + $D('noVNC_cursor').disabled = connected; + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + $D('noVNC_shared').disabled = connected; + $D('noVNC_view_only').disabled = connected; + $D('noVNC_path').disabled = connected; + $D('noVNC_repeaterID').disabled = connected; + + if (connected) { + UI.setViewClip(); + UI.setMouseButton(1); + $D('clipboardButton').style.display = "inline"; + $D('showKeyboard').style.display = "inline"; + $D('noVNC_extra_keys').style.display = ""; + $D('sendCtrlAltDelButton').style.display = "inline"; + } else { + UI.setMouseButton(); + $D('clipboardButton').style.display = "none"; + $D('showKeyboard').style.display = "none"; + $D('noVNC_extra_keys').style.display = "none"; + $D('sendCtrlAltDelButton').style.display = "none"; + UI.updateXvpVisualState(0); + } + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.setViewDrag(false); + + switch (UI.rfb_state) { + case 'fatal': + case 'failed': + case 'loaded': + case 'disconnected': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + break; + default: + $D('connectButton').style.display = "none"; + $D('disconnectButton').style.display = ""; + break; + } + + //Util.Debug("<< updateVisualState"); + }, + + // Disable/enable XVP button + updateXvpVisualState: function(ver) { + if (ver >= 1) { + $D('xvpButton').style.display = 'inline'; + } else { + $D('xvpButton').style.display = 'none'; + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + } + }, + + // Display the desktop name in the document title + updateDocumentTitle: function(rfb, name) { + document.title = name + " - noVNC"; + }, + + clipReceive: function(rfb, text) { + Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); + $D('noVNC_clipboard_text').value = text; + Util.Debug("<< UI.clipReceive"); + }, + + connect: function() { + UI.closeSettingsMenu(); + UI.toggleConnectPanel(); + var host = $D('noVNC_host').value; + var port = $D('noVNC_port').value; + var password = $D('noVNC_password').value; + var path = $D('noVNC_path').value; + if ((!host) || (!port)) { + throw new Error("Must set host and port"); + } + + UI.rfb.set_encrypt(UI.getSetting('encrypt')); + UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + UI.rfb.set_shared(UI.getSetting('shared')); + UI.rfb.set_view_only(UI.getSetting('view_only')); + UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + + UI.rfb.connect(host, port, password, path); + + //Close dialog. + setTimeout(UI.setBarPosition, 100); + $D('noVNC_logo').style.display = "none"; + }, + + disconnect: function() { + UI.closeSettingsMenu(); + UI.rfb.disconnect(); + + $D('noVNC_logo').style.display = "block"; + UI.connSettingsOpen = false; + UI.toggleConnectPanel(); + }, + + displayBlur: function() { + UI.rfb.get_keyboard().set_focused(false); + UI.rfb.get_mouse().set_focused(false); + }, + + displayFocus: function() { + UI.rfb.get_keyboard().set_focused(true); + UI.rfb.get_mouse().set_focused(true); + }, + + clipClear: function() { + $D('noVNC_clipboard_text').value = ""; + UI.rfb.clipboardPasteFrom(""); + }, + + clipSend: function() { + var text = $D('noVNC_clipboard_text').value; + Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Util.Debug("<< UI.clipSend"); + }, + + // Enable/disable and configure viewport clipping + setViewClip: function(clip) { + var display; + if (UI.rfb) { + display = UI.rfb.get_display(); + } else { + return; + } + + var cur_clip = display.get_viewport(); + + if (typeof(clip) !== 'boolean') { + // Use current setting + clip = UI.getSetting('clip'); + } + + if (clip && !cur_clip) { + // Turn clipping on + UI.updateSetting('clip', true); + } else if (!clip && cur_clip) { + // Turn clipping off + UI.updateSetting('clip', false); + display.set_viewport(false); + $D('noVNC_canvas').style.position = 'static'; + display.viewportChange(); + } + if (UI.getSetting('clip')) { + // If clipping, update clipping settings + $D('noVNC_canvas').style.position = 'absolute'; + var pos = Util.getPosition($D('noVNC_canvas')); + var new_w = window.innerWidth - pos.x; + var new_h = window.innerHeight - pos.y; + display.set_viewport(true); + display.viewportChange(0, 0, new_w, new_h); + } + }, + + // Toggle/set/unset the viewport drag/move button + setViewDrag: function(drag) { + var vmb = $D('noVNC_view_drag_button'); + if (!UI.rfb) { return; } + + if (UI.rfb_state === 'normal' && + UI.rfb.get_display().get_viewport()) { + vmb.style.display = "inline"; + } else { + vmb.style.display = "none"; + } + + if (typeof(drag) === "undefined" || + typeof(drag) === "object") { + // If not specified, then toggle + drag = !UI.rfb.get_viewportDrag(); + } + if (drag) { + vmb.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } else { + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + }, + + // On touch devices, show the OS keyboard + showKeyboard: function() { + var kbi = $D('keyboardinput'); + var skb = $D('showKeyboard'); + var l = kbi.value.length; + if(UI.keyboardVisible === false) { + kbi.focus(); + try { kbi.setSelectionRange(l, l); } // Move the caret to the end + catch (err) {} // setSelectionRange is undefined in Google Chrome + UI.keyboardVisible = true; + skb.className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === true) { + kbi.blur(); + skb.className = "noVNC_status_button"; + UI.keyboardVisible = false; + } + }, + + keepKeyboard: function() { + clearTimeout(UI.hideKeyboardTimeout); + if(UI.keyboardVisible === true) { + $D('keyboardinput').focus(); + $D('showKeyboard').className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === false) { + $D('keyboardinput').blur(); + $D('showKeyboard').className = "noVNC_status_button"; + } + }, + + keyboardinputReset: function() { + var kbi = $D('keyboardinput'); + kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); + UI.lastKeyboardinput = kbi.value; + }, + + // When normal keyboard events are left uncought, use the input events from + // the keyboardinput element instead and generate the corresponding key events. + // This code is required since some browsers on Android are inconsistent in + // sending keyCodes in the normal keyboard events when using on screen keyboards. + keyInput: function(event) { + var newValue = event.target.value; + var oldValue = UI.lastKeyboardinput; + + var newLen; + try { + // Try to check caret position since whitespace at the end + // will not be considered by value.length in some browsers + newLen = Math.max(event.target.selectionStart, newValue.length); + } catch (err) { + // selectionStart is undefined in Google Chrome + newLen = newValue.length; + } + var oldLen = oldValue.length; + + var backspaces; + var inputs = newLen - oldLen; + if (inputs < 0) { + backspaces = -inputs; + } else { + backspaces = 0; + } + + // Compare the old string with the new to account for + // text-corrections or other input that modify existing text + var i; + for (i = 0; i < Math.min(oldLen, newLen); i++) { + if (newValue.charAt(i) != oldValue.charAt(i)) { + inputs = newLen - i; + backspaces = oldLen - i; + break; + } + } + + // Send the key events + for (i = 0; i < backspaces; i++) { + UI.rfb.sendKey(XK_BackSpace); + } + for (i = newLen - inputs; i < newLen; i++) { + UI.rfb.sendKey(newValue.charCodeAt(i)); + } + + // Control the text content length in the keyboardinput element + if (newLen > 2 * UI.defaultKeyboardinputLen) { + UI.keyboardinputReset(); + } else if (newLen < 1) { + // There always have to be some text in the keyboardinput + // element with which backspace can interact. + UI.keyboardinputReset(); + // This sometimes causes the keyboard to disappear for a second + // but it is required for the android keyboard to recognize that + // text has been added to the field + event.target.blur(); + // This has to be ran outside of the input handler in order to work + setTimeout(function() { UI.keepKeyboard(); }, 0); + } else { + UI.lastKeyboardinput = newValue; + } + }, + + keyInputBlur: function() { + $D('showKeyboard').className = "noVNC_status_button"; + //Weird bug in iOS if you change keyboardVisible + //here it does not actually occur so next time + //you click keyboard icon it doesnt work. + UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100); + }, + + showExtraKeys: function() { + UI.keepKeyboard(); + if(UI.extraKeysVisible === false) { + $D('toggleCtrlButton').style.display = "inline"; + $D('toggleAltButton').style.display = "inline"; + $D('sendTabButton').style.display = "inline"; + $D('sendEscButton').style.display = "inline"; + $D('showExtraKeysButton').className = "noVNC_status_button_selected"; + UI.extraKeysVisible = true; + } else if(UI.extraKeysVisible === true) { + $D('toggleCtrlButton').style.display = ""; + $D('toggleAltButton').style.display = ""; + $D('sendTabButton').style.display = ""; + $D('sendEscButton').style.display = ""; + $D('showExtraKeysButton').className = "noVNC_status_button"; + UI.extraKeysVisible = false; + } + }, + + toggleCtrl: function() { + UI.keepKeyboard(); + if(UI.ctrlOn === false) { + UI.rfb.sendKey(XK_Control_L, true); + $D('toggleCtrlButton').className = "noVNC_status_button_selected"; + UI.ctrlOn = true; + } else if(UI.ctrlOn === true) { + UI.rfb.sendKey(XK_Control_L, false); + $D('toggleCtrlButton').className = "noVNC_status_button"; + UI.ctrlOn = false; + } + }, + + toggleAlt: function() { + UI.keepKeyboard(); + if(UI.altOn === false) { + UI.rfb.sendKey(XK_Alt_L, true); + $D('toggleAltButton').className = "noVNC_status_button_selected"; + UI.altOn = true; + } else if(UI.altOn === true) { + UI.rfb.sendKey(XK_Alt_L, false); + $D('toggleAltButton').className = "noVNC_status_button"; + UI.altOn = false; + } + }, + + sendTab: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Tab); + }, + + sendEsc: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Escape); + }, + + setKeyboard: function() { + UI.keyboardVisible = false; + }, + + // iOS < Version 5 does not support position fixed. Javascript workaround: + setOnscroll: function() { + window.onscroll = function() { + UI.setBarPosition(); + }; + }, + + setResize: function () { + window.onResize = function() { + UI.setBarPosition(); + }; + }, + + //Helper to add options to dropdown. + addOption: function(selectbox, text, value) { + var optn = document.createElement("OPTION"); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, + + setBarPosition: function() { + $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; + $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; + + var vncwidth = $D('noVNC_screen').style.offsetWidth; + $D('noVNC-control-bar').style.width = vncwidth + 'px'; + } + + }; +})(); diff --git a/webclients/novnc/include/util.js b/webclients/novnc/include/util.js index 57ccb54..909d04b 100644 --- a/webclients/novnc/include/util.js +++ b/webclients/novnc/include/util.js @@ -1,14 +1,13 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin - * Licensed under LGPL-3 (see LICENSE.txt) + * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. */ -"use strict"; -/*jslint bitwise: false, white: false */ -/*global window, console, document, navigator, ActiveXObject */ +/* jshint white: false, nonstandard: true */ +/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */ // Globals defined here var Util = {}; @@ -18,61 +17,163 @@ var Util = {}; * Make arrays quack */ -Array.prototype.push8 = function (num) { - this.push(num & 0xFF); +var addFunc = function (cl, name, func) { + if (!cl.prototype[name]) { + Object.defineProperty(cl.prototype, name, { enumerable: false, value: func }); + } }; -Array.prototype.push16 = function (num) { +addFunc(Array, 'push8', function (num) { + "use strict"; + this.push(num & 0xFF); +}); + +addFunc(Array, 'push16', function (num) { + "use strict"; this.push((num >> 8) & 0xFF, - (num ) & 0xFF ); -}; -Array.prototype.push32 = function (num) { + num & 0xFF); +}); + +addFunc(Array, 'push32', function (num) { + "use strict"; this.push((num >> 24) & 0xFF, (num >> 16) & 0xFF, (num >> 8) & 0xFF, - (num ) & 0xFF ); -}; + num & 0xFF); +}); // IE does not support map (even in IE9) //This prototype is provided by the Mozilla foundation and //is distributed under the MIT license. //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -if (!Array.prototype.map) -{ - Array.prototype.map = function(fun /*, thisp*/) - { +addFunc(Array, 'map', function (fun /*, thisp*/) { + "use strict"; var len = this.length; - if (typeof fun != "function") - throw new TypeError(); + if (typeof fun != "function") { + throw new TypeError(); + } var res = new Array(len); var thisp = arguments[1]; - for (var i = 0; i < len; i++) - { - if (i in this) - res[i] = fun.call(thisp, this[i], i, this); + for (var i = 0; i < len; i++) { + if (i in this) { + res[i] = fun.call(thisp, this[i], i, this); + } } return res; - }; +}); + +// IE <9 does not support indexOf +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Array, 'indexOf', function (elt /*, from*/) { + "use strict"; + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } + + for (; from < len; from++) { + if (from in this && + this[from] === elt) { + return from; + } + } + return -1; +}); + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +if (!Object.keys) { + Object.keys = (function () { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + })(); } -// +// PhantomJS 1.x doesn't support bind, +// so leave this in until PhantomJS 2.0 is released +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Function, 'bind', function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError("Function.prototype.bind - " + + "what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; +}); + +// // requestAnimationFrame shim with setTimeout fallback // -window.requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback){ +window.requestAnimFrame = (function () { + "use strict"; + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { window.setTimeout(callback, 1000 / 60); }; })(); -/* +/* * ------------------------------------------------------ * Namespaced in Util * ------------------------------------------------------ @@ -84,6 +185,7 @@ window.requestAnimFrame = (function(){ Util._log_level = 'warn'; Util.init_logging = function (level) { + "use strict"; if (typeof level === 'undefined') { level = Util._log_level; } else { @@ -94,26 +196,34 @@ Util.init_logging = function (level) { window.console = { 'log' : window.opera.postError, 'warn' : window.opera.postError, - 'error': window.opera.postError }; + 'error': window.opera.postError + }; } else { window.console = { - 'log' : function(m) {}, - 'warn' : function(m) {}, - 'error': function(m) {}}; + 'log' : function (m) {}, + 'warn' : function (m) {}, + 'error': function (m) {} + }; } } Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; + /* jshint -W086 */ switch (level) { - case 'debug': Util.Debug = function (msg) { console.log(msg); }; - case 'info': Util.Info = function (msg) { console.log(msg); }; - case 'warn': Util.Warn = function (msg) { console.warn(msg); }; - case 'error': Util.Error = function (msg) { console.error(msg); }; + case 'debug': + Util.Debug = function (msg) { console.log(msg); }; + case 'info': + Util.Info = function (msg) { console.log(msg); }; + case 'warn': + Util.Warn = function (msg) { console.warn(msg); }; + case 'error': + Util.Error = function (msg) { console.error(msg); }; case 'none': break; default: - throw("invalid logging type '" + level + "'"); + throw new Error("invalid logging type '" + level + "'"); } + /* jshint +W086 */ }; Util.get_logging = function () { return Util._log_level; @@ -121,107 +231,279 @@ Util.get_logging = function () { // Initialize logging level Util.init_logging(); +Util.make_property = function (proto, name, mode, type) { + "use strict"; -// Set configuration default for Crockford style function namespaces -Util.conf_default = function(cfg, api, defaults, v, mode, type, defval, desc) { - var getter, setter; + var getter; + if (type === 'arr') { + getter = function (idx) { + if (typeof idx !== 'undefined') { + return this['_' + name][idx]; + } else { + return this['_' + name]; + } + }; + } else { + getter = function () { + return this['_' + name]; + }; + } - // Default getter function - getter = function (idx) { - if ((type in {'arr':1, 'array':1}) && - (typeof idx !== 'undefined')) { - return cfg[v][idx]; + var make_setter = function (process_val) { + if (process_val) { + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = process_val(val); + } else { + this['_' + name] = process_val(val); + } + }; } else { - return cfg[v]; + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = val; + } else { + this['_' + name] = val; + } + }; } }; - // Default setter function - setter = function (val, idx) { - if (type in {'boolean':1, 'bool':1}) { - if ((!val) || (val in {'0':1, 'no':1, 'false':1})) { - val = false; + var setter; + if (type === 'bool') { + setter = make_setter(function (val) { + if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) { + return false; } else { - val = true; + return true; } - } else if (type in {'integer':1, 'int':1}) { - val = parseInt(val, 10); - } else if (type === 'str') { - val = String(val); - } else if (type === 'func') { + }); + } else if (type === 'int') { + setter = make_setter(function (val) { return parseInt(val, 10); }); + } else if (type === 'float') { + setter = make_setter(parseFloat); + } else if (type === 'str') { + setter = make_setter(String); + } else if (type === 'func') { + setter = make_setter(function (val) { if (!val) { - val = function () {}; + return function () {}; + } else { + return val; } - } - if (typeof idx !== 'undefined') { - cfg[v][idx] = val; - } else { - cfg[v] = val; - } - }; - - // Set the description - api[v + '_description'] = desc; + }); + } else if (type === 'arr' || type === 'dom' || type == 'raw') { + setter = make_setter(); + } else { + throw new Error('Unknown property type ' + type); // some sanity checking + } - // Set the getter function - if (typeof api['get_' + v] === 'undefined') { - api['get_' + v] = getter; + // set the getter + if (typeof proto['get_' + name] === 'undefined') { + proto['get_' + name] = getter; } - // Set the setter function with extra sanity checks - if (typeof api['set_' + v] === 'undefined') { - api['set_' + v] = function (val, idx) { - if (mode in {'RO':1, 'ro':1}) { - throw(v + " is read-only"); - } else if ((mode in {'WO':1, 'wo':1}) && - (typeof cfg[v] !== 'undefined')) { - throw(v + " can only be set once"); - } - setter(val, idx); - }; + // set the setter if needed + if (typeof proto['set_' + name] === 'undefined') { + if (mode === 'rw') { + proto['set_' + name] = setter; + } else if (mode === 'wo') { + proto['set_' + name] = function (val, idx) { + if (typeof this['_' + name] !== 'undefined') { + throw new Error(name + " can only be set once"); + } + setter.call(this, val, idx); + }; + } } - // Set the default value - if (typeof defaults[v] !== 'undefined') { - defval = defaults[v]; - } else if ((type in {'arr':1, 'array':1}) && - (! (defval instanceof Array))) { - defval = []; + // make a special setter that we can use in set defaults + proto['_raw_set_' + name] = function (val, idx) { + setter.call(this, val, idx); + //delete this['_init_set_' + name]; // remove it after use + }; +}; + +Util.make_properties = function (constructor, arr) { + "use strict"; + for (var i = 0; i < arr.length; i++) { + Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]); } - // Coerce existing setting to the right type - //Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]); - setter(defval); }; -// Set group of configuration defaults -Util.conf_defaults = function(cfg, api, defaults, arr) { +Util.set_defaults = function (obj, conf, defaults) { + var defaults_keys = Object.keys(defaults); + var conf_keys = Object.keys(conf); + var keys_obj = {}; var i; - for (i = 0; i < arr.length; i++) { - Util.conf_default(cfg, api, defaults, arr[i][0], arr[i][1], - arr[i][2], arr[i][3], arr[i][4]); + for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; } + for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; } + var keys = Object.keys(keys_obj); + + for (i = 0; i < keys.length; i++) { + var setter = obj['_raw_set_' + keys[i]]; + if (!setter) { + Util.Warn('Invalid property ' + keys[i]); + continue; + } + + if (keys[i] in conf) { + setter.call(obj, conf[keys[i]]); + } else { + setter.call(obj, defaults[keys[i]]); + } } }; +/* + * Decode from UTF-8 + */ +Util.decodeUTF8 = function (utf8string) { + "use strict"; + return decodeURIComponent(escape(utf8string)); +}; + + /* * Cross-browser routines */ -// Get DOM element position on page -Util.getPosition = function (obj) { - var x = 0, y = 0; - if (obj.offsetParent) { - do { - x += obj.offsetLeft; - y += obj.offsetTop; - obj = obj.offsetParent; - } while (obj); + +// Dynamically load scripts without using document.write() +// Reference: http://unixpapa.com/js/dyna.html +// +// Handles the case where load_scripts is invoked from a script that +// itself is loaded via load_scripts. Once all scripts are loaded the +// window.onscriptsloaded handler is called (if set). +Util.get_include_uri = function () { + return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/"; +}; +Util._loading_scripts = []; +Util._pending_scripts = []; +Util.load_scripts = function (files) { + "use strict"; + var head = document.getElementsByTagName('head')[0], script, + ls = Util._loading_scripts, ps = Util._pending_scripts; + + var loadFunc = function (e) { + while (ls.length > 0 && (ls[0].readyState === 'loaded' || + ls[0].readyState === 'complete')) { + // For IE, append the script to trigger execution + var s = ls.shift(); + //console.log("loaded script: " + s.src); + head.appendChild(s); + } + if (!this.readyState || + (Util.Engine.presto && this.readyState === 'loaded') || + this.readyState === 'complete') { + if (ps.indexOf(this) >= 0) { + this.onload = this.onreadystatechange = null; + //console.log("completed script: " + this.src); + ps.splice(ps.indexOf(this), 1); + + // Call window.onscriptsload after last script loads + if (ps.length === 0 && window.onscriptsload) { + window.onscriptsload(); + } + } + } + }; + + for (var f = 0; f < files.length; f++) { + script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = Util.get_include_uri() + files[f]; + //console.log("loading script: " + script.src); + script.onload = script.onreadystatechange = loadFunc; + // In-order script execution tricks + if (Util.Engine.trident) { + // For IE wait until readyState is 'loaded' before + // appending it which will trigger execution + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + ls.push(script); + } else { + // For webkit and firefox set async=false and append now + // https://developer.mozilla.org/en-US/docs/HTML/Element/script + script.async = false; + head.appendChild(script); + } + ps.push(script); } - return {'x': x, 'y': y}; }; + +// Get DOM element position on page +// This solution is based based on http://www.greywyvern.com/?post=331 +// Thanks to Brian Huisman AKA GreyWyvern! +Util.getPosition = (function () { + "use strict"; + function getStyle(obj, styleProp) { + var y; + if (obj.currentStyle) { + y = obj.currentStyle[styleProp]; + } else if (window.getComputedStyle) + y = window.getComputedStyle(obj, null)[styleProp]; + return y; + } + + function scrollDist() { + var myScrollTop = 0, myScrollLeft = 0; + var html = document.getElementsByTagName('html')[0]; + + // get the scrollTop part + if (html.scrollTop && document.documentElement.scrollTop) { + myScrollTop = html.scrollTop; + } else if (html.scrollTop || document.documentElement.scrollTop) { + myScrollTop = html.scrollTop + document.documentElement.scrollTop; + } else if (document.body.scrollTop) { + myScrollTop = document.body.scrollTop; + } else { + myScrollTop = 0; + } + + // get the scrollLeft part + if (html.scrollLeft && document.documentElement.scrollLeft) { + myScrollLeft = html.scrollLeft; + } else if (html.scrollLeft || document.documentElement.scrollLeft) { + myScrollLeft = html.scrollLeft + document.documentElement.scrollLeft; + } else if (document.body.scrollLeft) { + myScrollLeft = document.body.scrollLeft; + } else { + myScrollLeft = 0; + } + + return [myScrollLeft, myScrollTop]; + } + + return function (obj) { + var curleft = 0, curtop = 0, scr = obj, fixed = false; + while ((scr = scr.parentNode) && scr != document.body) { + curleft -= scr.scrollLeft || 0; + curtop -= scr.scrollTop || 0; + if (getStyle(scr, "position") == "fixed") { + fixed = true; + } + } + if (fixed && !window.opera) { + var scrDist = scrollDist(); + curleft += scrDist[0]; + curtop += scrDist[1]; + } + + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while ((obj = obj.offsetParent)); + + return {'x': curleft, 'y': curtop}; + }; +})(); + + // Get mouse event position in DOM element Util.getEventPosition = function (e, obj, scale) { + "use strict"; var evt, docX, docY, pos; //if (!e) evt = window.event; evt = (e ? e : window.event); @@ -239,36 +521,43 @@ Util.getEventPosition = function (e, obj, scale) { if (typeof scale === "undefined") { scale = 1; } - return {'x': (docX - pos.x) / scale, 'y': (docY - pos.y) / scale}; + var realx = docX - pos.x; + var realy = docY - pos.y; + var x = Math.max(Math.min(realx, obj.width - 1), 0); + var y = Math.max(Math.min(realy, obj.height - 1), 0); + return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; }; // Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events -Util.addEvent = function (obj, evType, fn){ - if (obj.attachEvent){ - var r = obj.attachEvent("on"+evType, fn); +Util.addEvent = function (obj, evType, fn) { + "use strict"; + if (obj.attachEvent) { + var r = obj.attachEvent("on" + evType, fn); return r; - } else if (obj.addEventListener){ - obj.addEventListener(evType, fn, false); + } else if (obj.addEventListener) { + obj.addEventListener(evType, fn, false); return true; } else { - throw("Handler could not be attached"); + throw new Error("Handler could not be attached"); } }; -Util.removeEvent = function(obj, evType, fn){ - if (obj.detachEvent){ - var r = obj.detachEvent("on"+evType, fn); +Util.removeEvent = function (obj, evType, fn) { + "use strict"; + if (obj.detachEvent) { + var r = obj.detachEvent("on" + evType, fn); return r; - } else if (obj.removeEventListener){ + } else if (obj.removeEventListener) { obj.removeEventListener(evType, fn, false); return true; } else { - throw("Handler could not be removed"); + throw new Error("Handler could not be removed"); } }; -Util.stopEvent = function(e) { +Util.stopEvent = function (e) { + "use strict"; if (e.stopPropagation) { e.stopPropagation(); } else { e.cancelBubble = true; } @@ -280,41 +569,88 @@ Util.stopEvent = function(e) { // Set browser engine versions. Based on mootools. Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; -Util.Engine = { - // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) - //'presto': (function() { - // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), - 'presto': (function() { return (!window.opera) ? false : true; }()), - - 'trident': (function() { - return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()), - 'webkit': (function() { - try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), - //'webkit': (function() { - // return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()), - 'gecko': (function() { - return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }()) -}; -if (Util.Engine.webkit) { - // Extract actual webkit version if available - Util.Engine.webkit = (function(v) { - var re = new RegExp('WebKit/([0-9\.]*) '); - v = (navigator.userAgent.match(re) || ['', v])[1]; - return parseFloat(v, 10); - })(Util.Engine.webkit); -} +(function () { + "use strict"; + // 'presto': (function () { return (!window.opera) ? false : true; }()), + var detectPresto = function () { + return !!window.opera; + }; + + // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); + var detectTrident = function () { + if (!window.ActiveXObject) { + return false; + } else { + if (window.XMLHttpRequest) { + return (document.querySelectorAll) ? 6 : 5; + } else { + return 4; + } + } + }; + + // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), + var detectInitialWebkit = function () { + try { + if (navigator.taintEnabled) { + return false; + } else { + if (Util.Features.xpath) { + return (Util.Features.query) ? 525 : 420; + } else { + return 419; + } + } + } catch (e) { + return false; + } + }; + + var detectActualWebkit = function (initial_ver) { + var re = /WebKit\/([0-9\.]*) /; + var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1]; + return parseFloat(str_ver, 10); + }; + + // 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }()) + var detectGecko = function () { + /* jshint -W041 */ + if (!document.getBoxObjectFor && window.mozInnerScreenX == null) { + return false; + } else { + return (document.getElementsByClassName) ? 19 : 18; + } + /* jshint +W041 */ + }; + + Util.Engine = { + // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) + //'presto': (function() { + // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), + 'presto': detectPresto(), + 'trident': detectTrident(), + 'webkit': detectInitialWebkit(), + 'gecko': detectGecko(), + }; + + if (Util.Engine.webkit) { + // Extract actual webkit version if available + Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit); + } +})(); -Util.Flash = (function(){ +Util.Flash = (function () { + "use strict"; var v, version; try { v = navigator.plugins['Shockwave Flash'].description; - } catch(err1) { + } catch (err1) { try { v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); - } catch(err2) { + } catch (err2) { v = '0 r0'; } } version = v.match(/\d+/g); return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; -}()); +}()); diff --git a/webclients/novnc/include/web-socket-js/WebSocketMain.swf b/webclients/novnc/include/web-socket-js/WebSocketMain.swf index 244c445..f286c81 100644 Binary files a/webclients/novnc/include/web-socket-js/WebSocketMain.swf and b/webclients/novnc/include/web-socket-js/WebSocketMain.swf differ diff --git a/webclients/novnc/include/web-socket-js/web_socket.js b/webclients/novnc/include/web-socket-js/web_socket.js index a133013..06cc5d0 100644 --- a/webclients/novnc/include/web-socket-js/web_socket.js +++ b/webclients/novnc/include/web-socket-js/web_socket.js @@ -1,49 +1,69 @@ // Copyright: Hiroshi Ichikawa // License: New BSD License // Reference: http://dev.w3.org/html5/websockets/ -// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol +// Reference: http://tools.ietf.org/html/rfc6455 (function() { - if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) return; - - var console = window.console; - if (!console || !console.log || !console.error) { - console = {log: function(){ }, error: function(){ }}; + if (window.WEB_SOCKET_FORCE_FLASH) { + // Keeps going. + } else if (window.WebSocket) { + return; + } else if (window.MozWebSocket) { + // Firefox. + window.WebSocket = MozWebSocket; + return; } - if (!swfobject.hasFlashPlayerVersion("10.0.0")) { - console.error("Flash Player >= 10.0.0 is required."); + var logger; + if (window.WEB_SOCKET_LOGGER) { + logger = WEB_SOCKET_LOGGER; + } else if (window.console && window.console.log && window.console.error) { + // In some environment, console is defined but console.log or console.error is missing. + logger = window.console; + } else { + logger = {log: function(){ }, error: function(){ }}; + } + + // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash. + if (swfobject.getFlashPlayerVersion().major < 10) { + logger.error("Flash Player >= 10.0.0 is required."); return; } if (location.protocol == "file:") { - console.error( + logger.error( "WARNING: web-socket-js doesn't work in file:///... URL " + "unless you set Flash Security Settings properly. " + "Open the page via Web server i.e. http://..."); } /** - * This class represents a faux web socket. + * Our own implementation of WebSocket class using Flash. * @param {string} url - * @param {string} protocol + * @param {array or string} protocols * @param {string} proxyHost * @param {int} proxyPort * @param {string} headers */ - WebSocket = function(url, protocol, proxyHost, proxyPort, headers) { + window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { var self = this; self.__id = WebSocket.__nextId++; WebSocket.__instances[self.__id] = self; self.readyState = WebSocket.CONNECTING; self.bufferedAmount = 0; self.__events = {}; + if (!protocols) { + protocols = []; + } else if (typeof protocols == "string") { + protocols = [protocols]; + } // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. // Otherwise, when onopen fires immediately, onopen is called before it is set. - setTimeout(function() { + self.__createTask = setTimeout(function() { WebSocket.__addTask(function() { + self.__createTask = null; WebSocket.__flash.create( - self.__id, url, protocol, proxyHost || null, proxyPort || 0, headers || null); + self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); }); }, 0); }; @@ -78,6 +98,12 @@ * Close this web socket gracefully. */ WebSocket.prototype.close = function() { + if (this.__createTask) { + clearTimeout(this.__createTask); + this.__createTask = null; + this.readyState = WebSocket.CLOSED; + return; + } if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { return; } @@ -131,7 +157,7 @@ events[i](event); } var handler = this["on" + event.type]; - if (handler) handler(event); + if (handler) handler.apply(this, [event]); }; /** @@ -139,16 +165,22 @@ * @param {Object} flashEvent */ WebSocket.prototype.__handleEvent = function(flashEvent) { + if ("readyState" in flashEvent) { this.readyState = flashEvent.readyState; } + if ("protocol" in flashEvent) { + this.protocol = flashEvent.protocol; + } var jsEvent; if (flashEvent.type == "open" || flashEvent.type == "error") { jsEvent = this.__createSimpleEvent(flashEvent.type); } else if (flashEvent.type == "close") { - // TODO implement jsEvent.wasClean jsEvent = this.__createSimpleEvent("close"); + jsEvent.wasClean = flashEvent.wasClean ? true : false; + jsEvent.code = flashEvent.code; + jsEvent.reason = flashEvent.reason; } else if (flashEvent.type == "message") { var data = decodeURIComponent(flashEvent.message); jsEvent = this.__createMessageEvent("message", data); @@ -157,6 +189,7 @@ } this.dispatchEvent(jsEvent); + }; WebSocket.prototype.__createSimpleEvent = function(type) { @@ -188,6 +221,9 @@ WebSocket.CLOSING = 2; WebSocket.CLOSED = 3; + // Field to check implementation of WebSocket. + WebSocket.__isFlashImplementation = true; + WebSocket.__initialized = false; WebSocket.__flash = null; WebSocket.__instances = {}; WebSocket.__tasks = []; @@ -207,16 +243,31 @@ * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. */ WebSocket.__initialize = function() { - if (WebSocket.__flash) return; + + if (WebSocket.__initialized) return; + WebSocket.__initialized = true; if (WebSocket.__swfLocation) { // For backword compatibility. window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; } if (!window.WEB_SOCKET_SWF_LOCATION) { - console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); return; } + if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR && + !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) && + WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) { + var swfHost = RegExp.$1; + if (location.host != swfHost) { + logger.error( + "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " + + "('" + location.host + "' != '" + swfHost + "'). " + + "See also 'How to host HTML file and SWF file in different domains' section " + + "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " + + "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;"); + } + } var container = document.createElement("div"); container.id = "webSocketContainer"; // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents @@ -250,9 +301,11 @@ null, function(e) { if (!e.success) { - console.error("[WebSocket] swfobject.embedSWF failed"); + logger.error("[WebSocket] swfobject.embedSWF failed"); } - }); + } + ); + }; /** @@ -287,7 +340,7 @@ WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); } } catch (e) { - console.error(e); + logger.error(e); } }, 0); return true; @@ -295,12 +348,12 @@ // Called by Flash. WebSocket.__log = function(message) { - console.log(decodeURIComponent(message)); + logger.log(decodeURIComponent(message)); }; // Called by Flash. WebSocket.__error = function(message) { - console.error(decodeURIComponent(message)); + logger.error(decodeURIComponent(message)); }; WebSocket.__addTask = function(task) { @@ -327,15 +380,12 @@ }; if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { - if (window.addEventListener) { - window.addEventListener("load", function(){ - WebSocket.__initialize(); - }, false); - } else { - window.attachEvent("onload", function(){ - WebSocket.__initialize(); - }); - } + // NOTE: + // This fires immediately if web_socket.js is dynamically loaded after + // the document is loaded. + swfobject.addDomLoadEvent(function() { + WebSocket.__initialize(); + }); } })(); diff --git a/webclients/novnc/include/websock.js b/webclients/novnc/include/websock.js index 20d51d6..1b89a91 100644 --- a/webclients/novnc/include/websock.js +++ b/webclients/novnc/include/websock.js @@ -1,7 +1,7 @@ /* * Websock: high-performance binary WebSockets * Copyright (C) 2012 Joel Martin - * Licensed under LGPL-3 (see LICENSE.txt) + * Licensed under MPL 2.0 (see LICENSE.txt) * * Websock is similar to the standard WebSocket object but Websock * enables communication with raw TCP sockets (i.e. the binary stream) @@ -14,7 +14,7 @@ * read binary data off of the receive queue. */ -/*jslint browser: true, bitwise: false, plusplus: false */ +/*jslint browser: true, bitwise: true */ /*global Util, Base64 */ @@ -35,326 +35,350 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { Websock_native = false; (function () { - function get_INCLUDE_URI() { - return (typeof INCLUDE_URI !== "undefined") ? - INCLUDE_URI : "include/"; - } - - var start = " --> - - -
- + title="Move/Drag Viewport">
- - - - - + + + + - + value="Keyboard" title="Show Keyboard"/> + + +
+ + + + + +
+
Loading
+
- - + + - + - + - + + title="Disconnect" />
- @@ -178,10 +196,6 @@
-
-
Loading
-
-

no
VNC

@@ -192,9 +206,8 @@
+ + - diff --git a/webclients/novnc/vnc_auto.html b/webclients/novnc/vnc_auto.html index 2b60d77..ff376fe 100644 --- a/webclients/novnc/vnc_auto.html +++ b/webclients/novnc/vnc_auto.html @@ -1,33 +1,66 @@ - - - noVNC - - - - - - - -
+ noVNC + + + + + + + + + + + + + + + + + + + + + + + + + +
- +
Loading
+ Loading +
+ + + + +
@@ -41,6 +74,11 @@ /*global window, $, Util, RFB, */ "use strict"; + // Load supporting scripts + Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", + "keysymdef.js", "keyboard.js", "input.js", "display.js", + "jsunzip.js", "rfb.js"]); + var rfb; function passwordRequired(rfb) { @@ -61,6 +99,18 @@ rfb.sendCtrlAltDel(); return false; } + function xvpShutdown() { + rfb.xvpShutdown(); + return false; + } + function xvpReboot() { + rfb.xvpReboot(); + return false; + } + function xvpReset() { + rfb.xvpReset(); + return false; + } function updateState(rfb, state, oldstate, msg) { var s, sb, cad, level; s = $D('noVNC_status'); @@ -75,8 +125,12 @@ default: level = "warn"; break; } - if (state === "normal") { cad.disabled = false; } - else { cad.disabled = true; } + if (state === "normal") { + cad.disabled = false; + } else { + cad.disabled = true; + xvpInit(0); + } if (typeof(msg) !== 'undefined') { sb.setAttribute("class", "noVNC_status_" + level); @@ -84,17 +138,42 @@ } } - window.onload = function () { + function xvpInit(ver) { + var xvpbuttons; + xvpbuttons = $D('noVNC_xvp_buttons'); + if (ver >= 1) { + xvpbuttons.style.display = 'inline'; + } else { + xvpbuttons.style.display = 'none'; + } + } + + window.onscriptsload = function () { var host, port, password, path, token; $D('sendCtrlAltDelButton').style.display = "inline"; $D('sendCtrlAltDelButton').onclick = sendCtrlAltDel; + $D('xvpShutdownButton').onclick = xvpShutdown; + $D('xvpRebootButton').onclick = xvpReboot; + $D('xvpResetButton').onclick = xvpReset; + WebUtil.init_logging(WebUtil.getQueryVar('logging', 'warn')); document.title = unescape(WebUtil.getQueryVar('title', 'noVNC')); // By default, use the host and port of server that served this file host = WebUtil.getQueryVar('host', window.location.hostname); port = WebUtil.getQueryVar('port', window.location.port); + // if port == 80 (or 443) then it won't be present and should be + // set manually + if (!port) { + if (window.location.protocol.substring(0,5) == 'https') { + port = 443; + } + else if (window.location.protocol.substring(0,4) == 'http') { + port = 80; + } + } + // If a token variable is passed in, set the parameter in a cookie. // This is used by nova-novncproxy. token = WebUtil.getQueryVar('token', null); @@ -119,7 +198,8 @@ 'local_cursor': WebUtil.getQueryVar('cursor', true), 'shared': WebUtil.getQueryVar('shared', true), 'view_only': WebUtil.getQueryVar('view_only', false), - 'updateState': updateState, + 'onUpdateState': updateState, + 'onXvpInit': xvpInit, 'onPasswordRequired': passwordRequired}); rfb.connect(host, port, password, path); }; @@ -127,4 +207,3 @@ -