44 Commits
v0.2 ... v0.3

Author SHA1 Message Date
Joel Martin
df4fa13b89 noVNC version 0.3
- Also add docs/release.txt instruction file.
2012-05-11 14:42:05 -05:00
Joel Martin
53dfab7fb7 Pull from websockify. Fix close code. 2012-05-10 22:10:01 -05:00
Joel Martin
bc28395abf Add/use display.drawImage which is viewport relative.
Fixes https://github.com/kanaka/noVNC/issues/163

When using an encoding with raw images (tight, tightPNG) we need to
draw those image relative to the viewport so that clipping works when
the viewport isn't at 0, 0.
2012-05-10 18:16:27 -05:00
Joel Martin
a117514183 Merge pull request #162 from toddfreese/master
RealVNC 4.6 Mac Support.
2012-05-10 15:31:14 -07:00
Todd Freese
58ad83878c Added support for RealVNC Mac. 2012-05-10 12:29:36 -05:00
Joel Martin
02d1f19b5f Pull from websockify.
Fix python 2.4 CClose exception handling.
2012-05-10 08:06:43 -05:00
Joel Martin
4910600b71 Merge pull request #158 from cloudbuilders/fix_rpc_backend
Fix rpc initialization issue
2012-04-30 15:16:24 -07:00
Anthony Young
ca78183171 Use explicit check for rpc.register_opts 2012-04-30 14:07:31 -07:00
Anthony Young
901e565503 Update to fix trunk nova issue with rpc library
* rpc flags now must be manually initialized.
2012-04-30 13:34:57 -07:00
Joel Martin
483157f87f Comment out duplicates in unicodeTable.
Unfortunately the values for those duplicate keys are not the same and
I'm not sure which ones are more correct. However, for now, I've
commented out the second occurrence.

This data is generated from /usr/include/X11/keysymdef.h using the
utils/u2x11 script.
2012-04-26 08:18:27 -05:00
Joel Martin
dcf1994d3e Don't treat onerror as a close.
The close event will also fire so trying to fail the connection twice
is unhelpful and hiding status information.
2012-04-25 20:13:25 -05:00
Joel Martin
c390011099 Update websockify to pull in close code/reason fixes. 2012-04-25 13:46:30 -05:00
Joel Martin
f8380ff939 Add code/reason to close event messages. Remove useless object print in error event. 2012-04-13 11:25:04 -05:00
Joel Martin
f736c3316a Merge pull request #149 from ohadlevy/patch-1
Update README.md to add Foreman project
2012-04-10 07:07:35 -07:00
Ohad Levy
cbd3435a07 Update README.md 2012-04-10 16:58:27 +03:00
Joel Martin
bd96e91932 Use page host:port as default for WebSocket host/port. 2012-04-03 16:58:21 -05:00
Anthony Young
0139b2562c Simplify nova-novncproxy related logic.
*  Don't clobber default host/port
2012-04-03 10:34:24 -07:00
Anthony Young
4c75210a4d Add nova-novncproxy
* Adds the nova-novncproxy binary, to provide support for vnc + OpensStack nova
 * Adds the ability to pass an auth token in via url, which is subsequently
   passed back to the proxy as a cookie.
2012-04-03 10:17:47 -07:00
Joel Martin
f84504bc63 Support Apple Remote Desktop.
https://github.com/kanaka/noVNC/issues/58
2012-03-23 12:21:23 -05:00
Joel Martin
c76b3e4b26 Add jsunzip.js to debian install list.
Thanks for catching this: https://github.com/abligh
2012-03-16 09:23:19 -07:00
Joel Martin
6671c7624d Release down/pressed keys when window loses focus.
May window managers have a keyboard shortcut that switch away from the
current desktop (e.g. desktop switcher). Unfortunately, when this
happens, the meta/control keys that are used with the shortcut will
send a down event to the browser, but the up event will never be sent
because the browser no longer has focus at the point when the up event
happens. This can cause weird stuck key issues for VNC clients (not
just noVNC). To get around this, we try and detect when the browser
loses focus and release any keys that are on the keyDownList.

As an aside, if you run into this situation (in noVNC or another VNC
client), you can unstick the state by pressing and releasing the Ctrl,
Shift, Alt, etc.

Addresses: https://github.com/kanaka/noVNC/pull/135
2012-03-14 11:10:06 -05:00
Joel Martin
ce86f5c954 Squelch noisy debug logs. 2012-03-13 20:24:24 -05:00
Joel Martin
c0c20581f5 Merge tight/tightPNG routine.
Mostly duplicate code so merge it and wrap with closures that pass in
the isTightPNG parameter.

Still detect and error if copy/filter when tightPNG.
2012-03-12 15:34:56 -07:00
Joel Martin
f2a495c944 Use standard citing format for contributors. 2012-03-12 00:34:53 -05:00
Joel Martin
b8dd87c757 Flesh out authors/contributors and licenses. 2012-03-11 14:39:23 -05:00
Joel Martin
d065cad99e General code formatting and cleanliness. 2012-03-10 20:32:01 -06:00
Joel Martin
a09a75e8f2 Stats: count pixels instead of just FBU.rects.
With last_rect, the rects count can be high until a last_rect
pseudo-encoding is received which messes with the timing stats. So
count up the number of pixels rendered and show timing after the pixel
count reaches the width*height of the screen.
2012-03-10 19:56:19 -06:00
Joel Martin
4cd0070a1c Cleanup vnc.js includes. 2012-03-10 19:54:56 -06:00
Joel Martin
35d7574b09 Merge commit 'd38db74abd0efa34f7297dc19bf603b7f765e0f5'
Conflicts:
	README.md
2012-03-10 19:52:11 -06:00
Mike Tinglof
d38db74abd add some documentation; default to existing websocket transport 2012-03-09 11:02:18 -05:00
Joel Martin
0c4f4b598c Don't swallow SSL EOF errors. 2012-02-20 16:33:08 -06:00
Joel Martin
c6f3919cb0 Better base64 illegal character output. 2012-02-20 15:48:39 -06:00
Mike Tinglof
9b75bcaada add tight zlib stream reset; add error if tight encoding is used w/o true color 2012-01-31 00:15:56 -05:00
Mike Tinglof
a14b8fae2a comment out per-decompress checksum and logging 2012-01-30 02:26:36 -05:00
Mike Tinglof
2cedf48397 add last rect special encoding; fix tight fill subencoding color handling; fix mono indexed rect handling 2012-01-30 02:19:18 -05:00
Mike Tinglof
c514fd5e1c don't need to copy palette data until we have all data for rect; change a few comments 2012-01-29 02:10:25 -05:00
Mike Tinglof
5ca5e2d8cd implement tight indexed rectangle; remove some debug code 2012-01-29 01:55:41 -05:00
Mike Tinglof
6fbc37489f fix handling of min compression size 2012-01-28 02:56:19 -05:00
Mike Tinglof
b0ac240f31 re-enable history buffer (used as sliding window for decompress) 2012-01-28 01:56:55 -05:00
Mike Tinglof
0fa6748c52 fix issue with parsing distance of more then 8 bits; convert to just supporting arrays for buffers 2012-01-26 14:35:36 -05:00
Mike Tinglof
a820f1267a added rgb image drawing, some zlib changes (huffman coding working, but lz77 not so much) 2012-01-25 18:09:55 -05:00
Mike Tinglof
c577ca2305 keep zlib history so we can decode as a stream 2012-01-24 21:18:29 -05:00
Mike T
de84e09854 basic framing for tight is working (decode not complete) 2012-01-24 13:56:33 -05:00
Mike
682f33a790 add javascript zlib 2012-01-24 13:49:49 -05:00
17 changed files with 1241 additions and 86 deletions

View File

@@ -13,6 +13,8 @@ version 3 with the following exceptions (all LGPL-3 compatible):
include/des.js : Various BSD style licenses
include/jsunzip.js : zlib/libpng license
include/web-socket-js/ : New BSD license. Source code at
http://github.com/gimite/web-socket-js

View File

@@ -10,13 +10,13 @@ Notable commits, announcements and news are posted to
@<a href="http://www.twitter.com/noVNC">noVNC</a>
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/), and [LibVNCServer](http://libvncserver.sourceforge.net). See [this wiki page](https://github.com/kanaka/noVNC/wiki/ProjectsCompanies-using-noVNC) for more info and links.
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) and [LibVNCServer](http://libvncserver.sourceforge.net). See [this wiki page](https://github.com/kanaka/noVNC/wiki/ProjectsCompanies-using-noVNC) for more info and links.
### Features
* Supports all modern browsers including mobile (iOS, Android)
* Supported VNC encodings: raw, copyrect, rre, hextile, tightPNG
* Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG
* WebSocket SSL/TLS encryption (i.e. "wss://") support
* 24-bit true color and 8 bit colour mapped
* Supports desktop resize notification/pseudo-encoding
@@ -26,7 +26,6 @@ their products including: [Ganeti Web Manager](http://code.osuosl.org/projects/g
* Easy site integration and theming (3 example themes included)
* Licensed under the [LGPLv3](http://www.gnu.org/licenses/lgpl.html)
### Screenshots
Running in Chrome before and after connecting:
@@ -85,3 +84,19 @@ a python proxy included ('websockify').
* [Troubleshooting noVNC](https://github.com/kanaka/noVNC/wiki/Troubleshooting) problems.
### Authors/Contributors
* noVNC : Joel Martin (github.com/kanaka)
* New UI and Icons : Chris Gordon
* Original Logo : Michael Sersen
* tight encoding : Michael Tinglof (Mercuri.ca)
* Included libraries:
* web-socket-js : Hiroshi Ichikawa (github.com/gimite/web-socket-js)
* as3crypto : Henri Torgemane (code.google.com/p/as3crypto)
* base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net)
* jsunzip : Erik Moller (github.com/operasoftware/jsunzip),
* tinflate : Joergen Ibsen (ibsensoftware.com)
* DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs)

10
debian/changelog vendored
View File

@@ -1,3 +1,13 @@
novnc (0.3) maverick; urgency=low
* add tight encoding support
* release pressed key when focus lost (fixes locked Alt key)
* Support Apple Remote Desktop
* Add nova/openstack proxy wrapper
* Better connection close handling/reporting
-- Joel Martin <github@martintribe.org> Fri, 11 May 2012 03:00:00 -0600
novnc (0.2) maverick; urgency=low
* Mobile device support with viewport clipping

View File

@@ -26,4 +26,5 @@ include/util.js /usr/share/novnc/include
include/vnc.js /usr/share/novnc/include
include/websock.js /usr/share/novnc/include
include/webutil.js /usr/share/novnc/include
include/jsunzip.js /usr/share/novnc/include
include/web-socket-js/* /usr/share/novnc/include/web-socket-js

View File

@@ -1 +1 @@
0.2
0.3

7
docs/release.txt Normal file
View File

@@ -0,0 +1,7 @@
- Update and commit docs/VERSION and debian/changelog
- Create version tag and tarball from tag
WVER=0.3
git tag ${WVER}
git archive --format=tar --prefix=novnc-${WVER}/ v${WVER} > novnc-${WVER}.tar
gzip novnc-${WVER}.tar
- Upload tarball to repo

View File

@@ -116,7 +116,7 @@ decode: function (data, offset) {
padding = (data.charAt(i) === pad);
// Skip illegal characters and whitespace
if (c === -1) {
console.error("Illegal character '" + data.charCodeAt(i) + "'");
console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
continue;
}

View File

@@ -20,7 +20,7 @@ var that = {}, // Public API methods
c_forceCanvas = false,
// Predefine function variables (jslint)
imageDataGet, bgrxImageData, cmapImageData,
imageDataGet, rgbImageData, bgrxImageData, cmapImageData,
setFillColor, rescale,
// The full frame buffer (logical canvas) size
@@ -497,6 +497,26 @@ that.finishTile = function() {
// 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;
/*
@@ -540,6 +560,15 @@ that.blitImage = function(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 () {
@@ -548,6 +577,12 @@ that.blitStringImage = function(str, x, 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.changeCursor = function(pixels, mask, hotx, hoty, w, h) {
if (conf.cursor_uri === false) {
Util.Warn("changeCursor called but no cursor data URI support");

View File

@@ -412,6 +412,26 @@ function onKeyUp(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
//
@@ -424,6 +444,9 @@ that.grab = function() {
Util.addEvent(c, 'keyup', onKeyUp);
Util.addEvent(c, 'keypress', onKeyPress);
// Release (key up) if window loses focus
Util.addEvent(window, 'blur', allKeysUp);
//Util.Debug("<< Keyboard.grab");
};
@@ -434,6 +457,10 @@ that.ungrab = function() {
Util.removeEvent(c, 'keydown', onKeyDown);
Util.removeEvent(c, 'keyup', onKeyUp);
Util.removeEvent(c, 'keypress', onKeyPress);
Util.removeEvent(window, 'blur', allKeysUp);
// Release (key up) all keys that are in a down state
allKeysUp();
//Util.Debug(">> Keyboard.ungrab");
};
@@ -1140,14 +1167,14 @@ unicodeTable = {
0x21D4 : 0x08cd,
0x21D2 : 0x08ce,
0x2261 : 0x08cf,
0x221A : 0x08d6,
//0x221A : 0x08d6,
0x2282 : 0x08da,
0x2283 : 0x08db,
0x2229 : 0x08dc,
0x222A : 0x08dd,
0x2227 : 0x08de,
0x2228 : 0x08df,
0x2202 : 0x08ef,
//0x2202 : 0x08ef,
0x0192 : 0x08f6,
0x2190 : 0x08fb,
0x2191 : 0x08fc,
@@ -1482,7 +1509,7 @@ unicodeTable = {
0x012D : 0x100012d,
0x01B6 : 0x10001b6,
0x01E7 : 0x10001e7,
0x01D2 : 0x10001d2,
//0x01D2 : 0x10001d2,
0x0275 : 0x1000275,
0x018F : 0x100018f,
0x0259 : 0x1000259,
@@ -1881,4 +1908,4 @@ unicodeTable = {
0x28fd : 0x10028fd,
0x28fe : 0x10028fe,
0x28ff : 0x10028ff
};
};

668
include/jsunzip.js Executable file
View File

@@ -0,0 +1,668 @@
/*
* JSUnzip
*
* Copyright (c) 2011 by Erik Moller
* All Rights Reserved
*
* This software is provided 'as-is', without any express
* or implied warranty. In no event will the authors be
* held liable for any damages arising from the use of
* this software.
*
* Permission is granted to anyone to use this software
* for any purpose, including commercial applications,
* and to alter it and redistribute it freely, subject to
* the following restrictions:
*
* 1. The origin of this software must not be
* misrepresented; you must not claim that you
* wrote the original software. If you use this
* software in a product, an acknowledgment in
* the product documentation would be appreciated
* but is not required.
*
* 2. Altered source versions must be plainly marked
* as such, and must not be misrepresented as
* being the original software.
*
* 3. This notice may not be removed or altered from
* any source distribution.
*/
var tinf;
function JSUnzip() {
this.getInt = function(offset, size) {
switch (size) {
case 4:
return (this.data.charCodeAt(offset + 3) & 0xff) << 24 |
(this.data.charCodeAt(offset + 2) & 0xff) << 16 |
(this.data.charCodeAt(offset + 1) & 0xff) << 8 |
(this.data.charCodeAt(offset + 0) & 0xff);
break;
case 2:
return (this.data.charCodeAt(offset + 1) & 0xff) << 8 |
(this.data.charCodeAt(offset + 0) & 0xff);
break;
default:
return this.data.charCodeAt(offset) & 0xff;
break;
}
};
this.getDOSDate = function(dosdate, dostime) {
var day = dosdate & 0x1f;
var month = ((dosdate >> 5) & 0xf) - 1;
var year = 1980 + ((dosdate >> 9) & 0x7f)
var second = (dostime & 0x1f) * 2;
var minute = (dostime >> 5) & 0x3f;
hour = (dostime >> 11) & 0x1f;
return new Date(year, month, day, hour, minute, second);
}
this.open = function(data) {
this.data = data;
this.files = [];
if (this.data.length < 22)
return { 'status' : false, 'error' : 'Invalid data' };
var endOfCentralDirectory = this.data.length - 22;
while (endOfCentralDirectory >= 0 && this.getInt(endOfCentralDirectory, 4) != 0x06054b50)
--endOfCentralDirectory;
if (endOfCentralDirectory < 0)
return { 'status' : false, 'error' : 'Invalid data' };
if (this.getInt(endOfCentralDirectory + 4, 2) != 0 || this.getInt(endOfCentralDirectory + 6, 2) != 0)
return { 'status' : false, 'error' : 'No multidisk support' };
var entriesInThisDisk = this.getInt(endOfCentralDirectory + 8, 2);
var centralDirectoryOffset = this.getInt(endOfCentralDirectory + 16, 4);
var globalCommentLength = this.getInt(endOfCentralDirectory + 20, 2);
this.comment = this.data.slice(endOfCentralDirectory + 22, endOfCentralDirectory + 22 + globalCommentLength);
var fileOffset = centralDirectoryOffset;
for (var i = 0; i < entriesInThisDisk; ++i) {
if (this.getInt(fileOffset + 0, 4) != 0x02014b50)
return { 'status' : false, 'error' : 'Invalid data' };
if (this.getInt(fileOffset + 6, 2) > 20)
return { 'status' : false, 'error' : 'Unsupported version' };
if (this.getInt(fileOffset + 8, 2) & 1)
return { 'status' : false, 'error' : 'Encryption not implemented' };
var compressionMethod = this.getInt(fileOffset + 10, 2);
if (compressionMethod != 0 && compressionMethod != 8)
return { 'status' : false, 'error' : 'Unsupported compression method' };
var lastModFileTime = this.getInt(fileOffset + 12, 2);
var lastModFileDate = this.getInt(fileOffset + 14, 2);
var lastModifiedDate = this.getDOSDate(lastModFileDate, lastModFileTime);
var crc = this.getInt(fileOffset + 16, 4);
// TODO: crc
var compressedSize = this.getInt(fileOffset + 20, 4);
var uncompressedSize = this.getInt(fileOffset + 24, 4);
var fileNameLength = this.getInt(fileOffset + 28, 2);
var extraFieldLength = this.getInt(fileOffset + 30, 2);
var fileCommentLength = this.getInt(fileOffset + 32, 2);
var relativeOffsetOfLocalHeader = this.getInt(fileOffset + 42, 4);
var fileName = this.data.slice(fileOffset + 46, fileOffset + 46 + fileNameLength);
var fileComment = this.data.slice(fileOffset + 46 + fileNameLength + extraFieldLength, fileOffset + 46 + fileNameLength + extraFieldLength + fileCommentLength);
if (this.getInt(relativeOffsetOfLocalHeader + 0, 4) != 0x04034b50)
return { 'status' : false, 'error' : 'Invalid data' };
var localFileNameLength = this.getInt(relativeOffsetOfLocalHeader + 26, 2);
var localExtraFieldLength = this.getInt(relativeOffsetOfLocalHeader + 28, 2);
var localFileContent = relativeOffsetOfLocalHeader + 30 + localFileNameLength + localExtraFieldLength;
this.files[fileName] =
{
'fileComment' : fileComment,
'compressionMethod' : compressionMethod,
'compressedSize' : compressedSize,
'uncompressedSize' : uncompressedSize,
'localFileContent' : localFileContent,
'lastModifiedDate' : lastModifiedDate
};
fileOffset += 46 + fileNameLength + extraFieldLength + fileCommentLength;
}
return { 'status' : true }
};
this.read = function(fileName) {
var fileInfo = this.files[fileName];
if (fileInfo) {
if (fileInfo.compressionMethod == 8) {
if (!tinf) {
tinf = new TINF();
tinf.init();
}
var result = tinf.uncompress(this.data, fileInfo.localFileContent);
if (result.status == tinf.OK)
return { 'status' : true, 'data' : result.data };
else
return { 'status' : false, 'error' : result.error };
} else {
return { 'status' : true, 'data' : this.data.slice(fileInfo.localFileContent, fileInfo.localFileContent + fileInfo.uncompressedSize) };
}
}
return { 'status' : false, 'error' : "File '" + fileName + "' doesn't exist in zip" };
};
};
/*
* tinflate - tiny inflate
*
* Copyright (c) 2003 by Joergen Ibsen / Jibz
* All Rights Reserved
*
* http://www.ibsensoftware.com/
*
* This software is provided 'as-is', without any express
* or implied warranty. In no event will the authors be
* held liable for any damages arising from the use of
* this software.
*
* Permission is granted to anyone to use this software
* for any purpose, including commercial applications,
* and to alter it and redistribute it freely, subject to
* the following restrictions:
*
* 1. The origin of this software must not be
* misrepresented; you must not claim that you
* wrote the original software. If you use this
* software in a product, an acknowledgment in
* the product documentation would be appreciated
* but is not required.
*
* 2. Altered source versions must be plainly marked
* as such, and must not be misrepresented as
* being the original software.
*
* 3. This notice may not be removed or altered from
* any source distribution.
*/
/*
* tinflate javascript port by Erik Moller in May 2011.
* emoller@opera.com
*
* read_bits() patched by mike@imidio.com to allow
* reading more then 8 bits (needed in some zlib streams)
*/
"use strict";
function TINF() {
this.OK = 0;
this.DATA_ERROR = (-3);
this.WINDOW_SIZE = 32768;
/* ------------------------------ *
* -- internal data structures -- *
* ------------------------------ */
this.TREE = function() {
this.table = new Array(16); /* table of code length counts */
this.trans = new Array(288); /* code -> symbol translation table */
};
this.DATA = function(that) {
this.source = '';
this.sourceIndex = 0;
this.tag = 0;
this.bitcount = 0;
this.dest = [];
this.history = [];
this.ltree = new that.TREE(); /* dynamic length/symbol tree */
this.dtree = new that.TREE(); /* dynamic distance tree */
};
/* --------------------------------------------------- *
* -- uninitialized global data (static structures) -- *
* --------------------------------------------------- */
this.sltree = new this.TREE(); /* fixed length/symbol tree */
this.sdtree = new this.TREE(); /* fixed distance tree */
/* extra bits and base tables for length codes */
this.length_bits = new Array(30);
this.length_base = new Array(30);
/* extra bits and base tables for distance codes */
this.dist_bits = new Array(30);
this.dist_base = new Array(30);
/* special ordering of code length codes */
this.clcidx = [
16, 17, 18, 0, 8, 7, 9, 6,
10, 5, 11, 4, 12, 3, 13, 2,
14, 1, 15
];
/* ----------------------- *
* -- utility functions -- *
* ----------------------- */
/* build extra bits and base tables */
this.build_bits_base = function(bits, base, delta, first)
{
var i, sum;
/* build bits table */
for (i = 0; i < delta; ++i) bits[i] = 0;
for (i = 0; i < 30 - delta; ++i) bits[i + delta] = Math.floor(i / delta);
/* build base table */
for (sum = first, i = 0; i < 30; ++i)
{
base[i] = sum;
sum += 1 << bits[i];
}
}
/* build the fixed huffman trees */
this.build_fixed_trees = function(lt, dt)
{
var i;
/* build fixed length tree */
for (i = 0; i < 7; ++i) lt.table[i] = 0;
lt.table[7] = 24;
lt.table[8] = 152;
lt.table[9] = 112;
for (i = 0; i < 24; ++i) lt.trans[i] = 256 + i;
for (i = 0; i < 144; ++i) lt.trans[24 + i] = i;
for (i = 0; i < 8; ++i) lt.trans[24 + 144 + i] = 280 + i;
for (i = 0; i < 112; ++i) lt.trans[24 + 144 + 8 + i] = 144 + i;
/* build fixed distance tree */
for (i = 0; i < 5; ++i) dt.table[i] = 0;
dt.table[5] = 32;
for (i = 0; i < 32; ++i) dt.trans[i] = i;
}
/* given an array of code lengths, build a tree */
this.build_tree = function(t, lengths, loffset, num)
{
var offs = new Array(16);
var i, sum;
/* clear code length count table */
for (i = 0; i < 16; ++i) t.table[i] = 0;
/* scan symbol lengths, and sum code length counts */
for (i = 0; i < num; ++i) t.table[lengths[loffset + i]]++;
t.table[0] = 0;
/* compute offset table for distribution sort */
for (sum = 0, i = 0; i < 16; ++i)
{
offs[i] = sum;
sum += t.table[i];
}
/* create code->symbol translation table (symbols sorted by code) */
for (i = 0; i < num; ++i)
{
if (lengths[loffset + i]) t.trans[offs[lengths[loffset + i]]++] = i;
}
}
/* ---------------------- *
* -- decode functions -- *
* ---------------------- */
/* get one bit from source stream */
this.getbit = function(d)
{
var bit;
/* check if tag is empty */
if (!d.bitcount--)
{
/* load next tag */
d.tag = d.source[d.sourceIndex++] & 0xff;
d.bitcount = 7;
}
/* shift bit out of tag */
bit = d.tag & 0x01;
d.tag >>= 1;
return bit;
}
/* read a num bit value from a stream and add base */
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;
}
/* given a data stream and a tree, decode a symbol */
this.decode_symbol = function(d, t)
{
while (d.bitcount < 16) {
d.tag = d.tag | (d.source[d.sourceIndex++] & 0xff) << d.bitcount;
d.bitcount += 8;
}
var sum = 0, cur = 0, len = 0;
do {
cur = 2 * cur + ((d.tag & (1 << len)) >> len);
++len;
sum += t.table[len];
cur -= t.table[len];
} while (cur >= 0);
d.tag >>= len;
d.bitcount -= len;
return t.trans[sum + cur];
}
/* given a data stream, decode dynamic trees from it */
this.decode_trees = function(d, lt, dt)
{
var code_tree = new this.TREE();
var lengths = new Array(288+32);
var hlit, hdist, hclen;
var i, num, length;
/* get 5 bits HLIT (257-286) */
hlit = this.read_bits(d, 5, 257);
/* get 5 bits HDIST (1-32) */
hdist = this.read_bits(d, 5, 1);
/* get 4 bits HCLEN (4-19) */
hclen = this.read_bits(d, 4, 4);
for (i = 0; i < 19; ++i) lengths[i] = 0;
/* read code lengths for code length alphabet */
for (i = 0; i < hclen; ++i)
{
/* get 3 bits code length (0-7) */
var clen = this.read_bits(d, 3, 0);
lengths[this.clcidx[i]] = clen;
}
/* build code length tree */
this.build_tree(code_tree, lengths, 0, 19);
/* decode code lengths for the dynamic trees */
for (num = 0; num < hlit + hdist; )
{
var sym = this.decode_symbol(d, code_tree);
switch (sym)
{
case 16:
/* copy previous code length 3-6 times (read 2 bits) */
{
var prev = lengths[num - 1];
for (length = this.read_bits(d, 2, 3); length; --length)
{
lengths[num++] = prev;
}
}
break;
case 17:
/* repeat code length 0 for 3-10 times (read 3 bits) */
for (length = this.read_bits(d, 3, 3); length; --length)
{
lengths[num++] = 0;
}
break;
case 18:
/* repeat code length 0 for 11-138 times (read 7 bits) */
for (length = this.read_bits(d, 7, 11); length; --length)
{
lengths[num++] = 0;
}
break;
default:
/* values 0-15 represent the actual code lengths */
lengths[num++] = sym;
break;
}
}
/* build dynamic trees */
this.build_tree(lt, lengths, 0, hlit);
this.build_tree(dt, lengths, hlit, hdist);
}
/* ----------------------------- *
* -- block inflate functions -- *
* ----------------------------- */
/* given a stream and two trees, inflate a block of data */
this.inflate_block_data = function(d, lt, dt)
{
// js optimization.
var ddest = d.dest;
var ddestlength = ddest.length;
while (1)
{
var sym = this.decode_symbol(d, lt);
/* check for end of block */
if (sym == 256)
{
return this.OK;
}
if (sym < 256)
{
ddest[ddestlength++] = sym; // ? String.fromCharCode(sym);
d.history.push(sym);
} else {
var length, dist, offs;
var i;
sym -= 257;
/* possibly get more bits from length code */
length = this.read_bits(d, this.length_bits[sym], this.length_base[sym]);
dist = this.decode_symbol(d, dt);
/* possibly get more bits from distance code */
offs = d.history.length - this.read_bits(d, this.dist_bits[dist], this.dist_base[dist]);
if (offs < 0)
throw ("Invalid zlib offset " + offs);
/* copy match */
for (i = offs; i < offs + length; ++i) {
//ddest[ddestlength++] = ddest[i];
ddest[ddestlength++] = d.history[i];
d.history.push(d.history[i]);
}
}
}
}
/* inflate an uncompressed block of data */
this.inflate_uncompressed_block = function(d)
{
var length, invlength;
var i;
if (d.bitcount > 7) {
var overflow = Math.floor(d.bitcount / 8);
d.sourceIndex -= overflow;
d.bitcount = 0;
d.tag = 0;
}
/* get length */
length = d.source[d.sourceIndex+1];
length = 256*length + d.source[d.sourceIndex];
/* get one's complement of length */
invlength = d.source[d.sourceIndex+3];
invlength = 256*invlength + d.source[d.sourceIndex+2];
/* check length */
if (length != (~invlength & 0x0000ffff)) return this.DATA_ERROR;
d.sourceIndex += 4;
/* copy block */
for (i = length; i; --i) {
d.history.push(d.source[d.sourceIndex]);
d.dest[d.dest.length] = d.source[d.sourceIndex++];
}
/* make sure we start next block on a byte boundary */
d.bitcount = 0;
return this.OK;
}
/* inflate a block of data compressed with fixed huffman trees */
this.inflate_fixed_block = function(d)
{
/* decode block using fixed trees */
return this.inflate_block_data(d, this.sltree, this.sdtree);
}
/* inflate a block of data compressed with dynamic huffman trees */
this.inflate_dynamic_block = function(d)
{
/* decode trees from stream */
this.decode_trees(d, d.ltree, d.dtree);
/* decode block using decoded trees */
return this.inflate_block_data(d, d.ltree, d.dtree);
}
/* ---------------------- *
* -- public functions -- *
* ---------------------- */
/* initialize global (static) data */
this.init = function()
{
/* build fixed huffman trees */
this.build_fixed_trees(this.sltree, this.sdtree);
/* build extra bits and base tables */
this.build_bits_base(this.length_bits, this.length_base, 4, 3);
this.build_bits_base(this.dist_bits, this.dist_base, 2, 1);
/* fix a special case */
this.length_bits[28] = 0;
this.length_base[28] = 258;
this.reset();
}
this.reset = function()
{
this.d = new this.DATA(this);
delete this.header;
}
/* inflate stream from source to dest */
this.uncompress = function(source, offset)
{
var d = this.d;
var bfinal;
/* initialise data */
d.source = source;
d.sourceIndex = offset;
d.bitcount = 0;
d.dest = [];
// Skip zlib header at start of stream
if (typeof this.header == 'undefined') {
this.header = this.read_bits(d, 16, 0);
/* byte 0: 0x78, 7 = 32k window size, 8 = deflate */
/* byte 1: check bits for header and other flags */
}
var blocks = 0;
do {
var btype;
var res;
/* read final block flag */
bfinal = this.getbit(d);
/* read block type (2 bits) */
btype = this.read_bits(d, 2, 0);
/* decompress block */
switch (btype)
{
case 0:
/* decompress uncompressed block */
res = this.inflate_uncompressed_block(d);
break;
case 1:
/* decompress block with fixed huffman trees */
res = this.inflate_fixed_block(d);
break;
case 2:
/* decompress block with dynamic huffman trees */
res = this.inflate_dynamic_block(d);
break;
default:
return { 'status' : this.DATA_ERROR };
}
if (res != this.OK) return { 'status' : this.DATA_ERROR };
blocks++;
} while (!bfinal && d.sourceIndex < d.source.length);
d.history = d.history.slice(-this.WINDOW_SIZE);
return { 'status' : this.OK, 'data' : d.dest };
}
};

View File

@@ -4,6 +4,9 @@
* Licensed under LGPL-3 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
* TIGHT decoder portion:
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
*/
/*jslint white: false, browser: true, bitwise: false, plusplus: false */
@@ -23,7 +26,7 @@ var that = {}, // Public API methods
pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests,
keyEvent, pointerEvent, clientCutText,
extract_data_uri, scan_tight_imgQ,
getTightCLength, extract_data_uri, scan_tight_imgQ,
keyPress, mouseButton, mouseMove,
checkEvents, // Overridable for testing
@@ -46,6 +49,7 @@ var that = {}, // Public API methods
// In preference order
encodings = [
['COPYRECT', 0x01 ],
['TIGHT', 0x07 ],
['TIGHT_PNG', -260 ],
['HEXTILE', 0x05 ],
['RRE', 0x02 ],
@@ -54,10 +58,12 @@ var that = {}, // Public API methods
['Cursor', -239 ],
// Psuedo-encoding settings
['JPEG_quality_lo', -32 ],
//['JPEG_quality_lo', -32 ],
['JPEG_quality_med', -26 ],
//['JPEG_quality_hi', -23 ],
['compress_lo', -255 ]
//['compress_hi', -247 ]
//['compress_lo', -255 ],
['compress_hi', -247 ],
['last_rect', -224 ]
],
encHandlers = {},
@@ -87,7 +93,8 @@ var that = {}, // Public API methods
encoding : 0,
subencoding : -1,
background : null,
imgQ : [] // TIGHT_PNG image queue
imgQ : [], // TIGHT_PNG image queue
zlibs : [] // TIGHT zlib streams
},
fb_Bpp = 4,
@@ -109,7 +116,8 @@ var that = {}, // Public API methods
fbu_rt_start : 0,
fbu_rt_total : 0,
fbu_rt_cnt : 0
fbu_rt_cnt : 0,
pixels : 0
},
test_mode = false,
@@ -226,21 +234,28 @@ function constructor() {
}
});
ws.on('close', function(e) {
Util.Warn("WebSocket on-close event");
var msg = "";
if (e.code) {
Util.Info("Close code: " + e.code + ", reason: " + e.reason + ", wasClean: " + e.wasClean);
msg = " (code: " + e.code;
if (e.reason) {
msg += ", reason: " + e.reason;
}
msg += ")";
}
if (rfb_state === 'disconnect') {
updateState('disconnected', 'VNC disconnected');
updateState('disconnected', 'VNC disconnected' + msg);
} else if (rfb_state === 'ProtocolVersion') {
fail('Failed to connect to server');
fail('Failed to connect to server' + msg);
} else if (rfb_state in {'failed':1, 'disconnected':1}) {
Util.Error("Received onclose while disconnected");
Util.Error("Received onclose while disconnected" + msg);
} else {
fail('Server disconnected');
fail('Server disconnected' + msg);
}
});
ws.on('error', function(e) {
fail("WebSock error: " + e);
Util.Warn("WebSocket on-error event");
//fail("WebSock reported an error");
});
@@ -270,14 +285,18 @@ function constructor() {
function connect() {
Util.Debug(">> RFB.connect");
var uri = "";
if (conf.encrypt) {
uri = "wss://";
var uri;
if (typeof UsingSocketIO !== "undefined") {
uri = "http://" + rfb_host + ":" + rfb_port + "/" + rfb_path;
} else {
uri = "ws://";
if (conf.encrypt) {
uri = "wss://";
} else {
uri = "ws://";
}
uri += rfb_host + ":" + rfb_port + "/" + rfb_path;
}
uri += rfb_host + ":" + rfb_port + "/" + rfb_path;
Util.Info("connecting to " + uri);
ws.open(uri);
@@ -296,6 +315,7 @@ init_vars = function() {
FBU.lines = 0; // RAW
FBU.tiles = 0; // HEXTILE
FBU.imgQ = []; // TIGHT_PNG image queue
FBU.zlibs = []; // TIGHT zlib encoders
mouse_buttonMask = 0;
mouse_arr = [];
@@ -303,6 +323,12 @@ init_vars = function() {
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
@@ -400,16 +426,16 @@ updateState = function(state, statusMsg) {
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.
// Do disconnect action, but stay in failed state
rfb_state = 'failed';
} else {
rfb_state = state;
}
cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
func("New state '" + rfb_state + "', was '" + oldstate + "'." + cmsg);
if (connTimer && (rfb_state !== 'connect')) {
Util.Debug("Clearing connect timer");
clearInterval(connTimer);
@@ -653,9 +679,11 @@ init_msg = function() {
switch (sversion) {
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);
}
@@ -1004,9 +1032,10 @@ framebufferUpdate = function() {
if (ret) {
encStats[FBU.encoding][0] += 1;
encStats[FBU.encoding][1] += 1;
timing.pixels += FBU.width * FBU.height;
}
if (FBU.rects === 0) {
if (FBU.rects === 0 || (timing.pixels >= (fb_width * fb_height))) {
if (((FBU.width === fb_width) &&
(FBU.height === fb_height)) ||
(timing.fbu_rt_start > 0)) {
@@ -1257,42 +1286,197 @@ encHandlers.HEXTILE = function display_hextile() {
};
encHandlers.TIGHT_PNG = function display_tight_png() {
//Util.Debug(">> display_tight_png");
var ctl, cmode, clength, getCLength, color, img;
//Util.Debug(" FBU.rects: " + FBU.rects);
//Util.Debug(" starting ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
// 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");
if (fb_depth === 1) {
fail("Tight protocol handler only implements true color mode");
}
var ctl, cmode, clength, color, img, data;
var filterId = -1, resetStreams = 0, streamId = -1;
var rQ = ws.get_rQ(), rQi = ws.get_rQi();
FBU.bytes = 1; // compression-control byte
if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; }
// Get 'compact length' header and data size
getCLength = 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;
var checksum = function(data) {
var sum=0, i;
for (i=0; i<data.length;i++) {
sum += data[i];
if (sum > 65536) sum -= 65536;
}
return sum;
}
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);
}
}
return [header, data];
};
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]);
if (raw) {
data = ws.rQshiftBytes(clength[1]);
} else {
data = decompress(ws.rQshiftBytes(clength[1]));
}
// 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];
}
}
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];
}
}
} 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];
}
}
}
FBU.imgQ.push({
'type': 'rgb',
'img': {'complete': true, '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];
} else {
clength = getTightCLength(ws.rQslice(1, 4));
}
FBU.bytes = 1 + clength[0] + clength[1];
if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
// Shift ctl, clength off
ws.rQshiftBytes(1 + clength[0]);
if (raw) {
data = ws.rQshiftBytes(clength[1]);
} else {
data = decompress(ws.rQshiftBytes(clength[1]));
}
FBU.imgQ.push({
'type': 'rgb',
'img': {'complete': true, 'data': data},
'x': FBU.x,
'y': FBU.y,
'width': FBU.width,
'height': FBU.height});
return true;
}
ctl = ws.rQpeek8();
switch (ctl >> 4) {
case 0x08: cmode = "fill"; break;
case 0x09: cmode = "jpeg"; break;
case 0x0A: cmode = "png"; break;
default: throw("Illegal basic compression received, ctl: " + ctl);
// 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 throw("Illegal tight compression received, ctl: " + ctl);
if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
throw("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 "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; }
@@ -1312,16 +1496,17 @@ encHandlers.TIGHT_PNG = function display_tight_png() {
'y': FBU.y,
'width': FBU.width,
'height': FBU.height,
'color': color});
'color': [color[2], color[1], color[0]] });
break;
case "jpeg":
case "png":
clength = getCLength(ws.rQslice(1, 4));
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(" png, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " + clength[0] + ", clength[1]: " + clength[1]);
//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.onload = scan_tight_imgQ;
@@ -1334,13 +1519,27 @@ encHandlers.TIGHT_PNG = function display_tight_png() {
extract_data_uri(ws.rQshiftBytes(clength[1]));
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);
}
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 = [];
@@ -1360,14 +1559,26 @@ scan_tight_imgQ = function() {
data = imgQ.shift();
if (data.type === 'fill') {
display.fillRect(data.x, data.y, data.width, data.height, data.color);
} else if (data.type === 'rgb') {
display.blitRgbImage(data.x, data.y, data.width, data.height, data.img.data, 0);
} else {
ctx.drawImage(data.img, data.x, data.y);
display.drawImage(data.img, data.x, data.y);
}
}
setTimeout(scan_tight_imgQ, scan_imgQ_rate);
}
};
encHandlers.TIGHT = function () { return display_tight(false); };
encHandlers.TIGHT_PNG = function () { return display_tight(true); };
encHandlers.last_rect = function last_rect() {
Util.Debug(">> set_desktopsize");
FBU.rects = 0;
Util.Debug("<< set_desktopsize");
return true;
};
encHandlers.DesktopSize = function set_desktopsize() {
Util.Debug(">> set_desktopsize");
fb_width = FBU.width;

View File

@@ -45,8 +45,8 @@ load: function() {
WebUtil.selectStylesheet(UI.getSetting('stylesheet'));
/* Populate the controls if defaults are provided in the URL */
UI.initSetting('host', '');
UI.initSetting('port', '');
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);

View File

@@ -36,6 +36,7 @@ function get_INCLUDE_URI() {
extra += start + "input.js" + end;
extra += start + "display.js" + end;
extra += start + "rfb.js" + end;
extra += start + "jsunzip.js" + end;
document.write(extra);
}());

153
utils/nova-novncproxy Executable file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 Openstack, LLC.
# All Rights Reserved.
#
# 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.
#!/usr/bin/env python
'''
Websocket proxy that is compatible with Openstack Nova.
Leverages wsproxy.py by Joel Martin
'''
import Cookie
import socket
import sys
import wsproxy
from nova import context
from nova import flags
from nova import rpc
from nova import utils
from nova.openstack.common import cfg
opts = [
cfg.BoolOpt('record',
default=False,
help='Record sessions to FILE.[session_number]'),
cfg.BoolOpt('daemon',
default=False,
help='Become a daemon (background process)'),
cfg.BoolOpt('ssl_only',
default=False,
help='Disallow non-encrypted connections'),
cfg.BoolOpt('source_is_ipv6',
default=False,
help='Source is ipv6'),
cfg.StrOpt('cert',
default='self.pem',
help='SSL certificate file'),
cfg.StrOpt('key',
default=None,
help='SSL key file (if separate from cert)'),
cfg.StrOpt('web',
default='.',
help='Run webserver on same port. Serve files from DIR.'),
cfg.StrOpt('novncproxy_host',
default='0.0.0.0',
help='Host on which to listen for incoming requests'),
cfg.IntOpt('novncproxy_port',
default=6080,
help='Port on which to listen for incoming requests'),
]
FLAGS = flags.FLAGS
FLAGS.register_cli_opts(opts)
# As of nova commit 0b11668e64450039dc071a4a123abd02206f865f we must
# manually register the rpc library
if hasattr(rpc, 'register_opts'):
rpc.register_opts(FLAGS)
class NovaWebSocketProxy(wsproxy.WebSocketProxy):
def __init__(self, *args, **kwargs):
wsproxy.WebSocketProxy.__init__(self, *args, **kwargs)
def new_client(self):
"""
Called after a new WebSocket connection has been established.
"""
cookie = Cookie.SimpleCookie()
cookie.load(self.headers.getheader('cookie'))
token = cookie['token'].value
ctxt = context.get_admin_context()
connect_info = rpc.call(ctxt, 'consoleauth',
{'method': 'check_token',
'args': {'token': token}})
if not connect_info:
raise Exception("Invalid Token")
host = connect_info['host']
port = int(connect_info['port'])
# Connect to the target
self.msg("connecting to: %s:%s" % (
host, port))
tsock = self.socket(host, port,
connect=True)
# Handshake as necessary
if connect_info.get('internal_access_path'):
tsock.send("CONNECT %s HTTP/1.1\r\n\r\n" %
connect_info['internal_access_path'])
while True:
data = tsock.recv(4096, socket.MSG_PEEK)
if data.find("\r\n\r\n") != -1:
if not data.split("\r\n")[0].find("200"):
raise Exception("Invalid Connection Info")
tsock.recv(len(data))
break
if self.verbose and not self.daemon:
print(self.traffic_legend)
# Start proxying
try:
self.do_proxy(tsock)
except:
if tsock:
tsock.shutdown(socket.SHUT_RDWR)
tsock.close()
self.vmsg("%s:%s: Target closed" % (host, port))
raise
if __name__ == '__main__':
if FLAGS.ssl_only and not os.path.exists(FLAGS.cert):
parser.error("SSL only and %s not found" % FLAGS.cert)
# Setup flags
utils.default_flagfile()
FLAGS(sys.argv)
# Create and start the NovaWebSockets proxy
server = NovaWebSocketProxy(listen_host=FLAGS.novncproxy_host,
listen_port=FLAGS.novncproxy_port,
source_is_ipv6=FLAGS.source_is_ipv6,
verbose=FLAGS.verbose,
cert=FLAGS.cert,
key=FLAGS.key,
ssl_only=FLAGS.ssl_only,
daemon=FLAGS.daemon,
record=FLAGS.record,
web=FLAGS.web,
target_host='ignore',
target_port='ignore',
wrap_mode='exit',
wrap_cmd=None)
server.start_server()

View File

@@ -92,9 +92,14 @@ Sec-WebSocket-Accept: %s\r
policy_response = """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\n"""
# An exception before the WebSocket connection was established
class EClose(Exception):
pass
# An exception while the WebSocket client was connected
class CClose(Exception):
pass
def __init__(self, listen_host='', listen_port=None, source_is_ipv6=False,
verbose=False, cert='', key='', ssl_only=None,
daemon=False, record='', web='',
@@ -303,8 +308,8 @@ Sec-WebSocket-Accept: %s\r
'length' : 0,
'payload' : None,
'left' : 0,
'close_code' : None,
'close_reason' : None}
'close_code' : 1000,
'close_reason' : ''}
blen = len(buf)
f['left'] = blen
@@ -357,7 +362,7 @@ Sec-WebSocket-Accept: %s\r
if f['opcode'] == 0x08:
if f['length'] >= 2:
f['close_code'] = unpack_from(">H", f['payload'])
f['close_code'] = unpack_from(">H", f['payload'])[0]
if f['length'] > 3:
f['close_reason'] = f['payload'][2:]
@@ -470,7 +475,7 @@ Sec-WebSocket-Accept: %s\r
buf = self.client.recv(self.buffer_size)
if len(buf) == 0:
closed = "Client closed abruptly"
closed = {'code': 1000, 'reason': "Client closed abruptly"}
return bufs, closed
if self.recv_part:
@@ -492,14 +497,14 @@ Sec-WebSocket-Accept: %s\r
break
else:
if frame['opcode'] == 0x8: # connection close
closed = "Client closed, reason: %s - %s" % (
frame['close_code'],
frame['close_reason'])
closed = {'code': frame['close_code'],
'reason': frame['close_reason']}
break
else:
if buf[0:2] == s2b('\xff\x00'):
closed = "Client sent orderly close frame"
closed = {'code': 1000,
'reason': "Client sent orderly close frame"}
break
elif buf[0:2] == s2b('\x00\xff'):
@@ -532,13 +537,11 @@ Sec-WebSocket-Accept: %s\r
return bufs, closed
def send_close(self, code=None, reason=''):
def send_close(self, code=1000, reason=''):
""" Send a WebSocket orderly close frame. """
if self.version.startswith("hybi"):
msg = s2b('')
if code != None:
msg = pack(">H%ds" % (len(reason)), code)
msg = pack(">H%ds" % len(reason), code, reason)
buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False)
self.client.send(buf)
@@ -603,7 +606,10 @@ Sec-WebSocket-Accept: %s\r
except ssl.SSLError:
_, x, _ = sys.exc_info()
if x.args[0] == ssl.SSL_ERROR_EOF:
raise self.EClose("")
if len(x.args) > 1:
raise self.EClose(x.args[1])
else:
raise self.EClose("Got SSL_ERROR_EOF")
else:
raise
@@ -759,6 +765,11 @@ Sec-WebSocket-Accept: %s\r
self.ws_connection = True
self.new_client()
except self.CClose:
# Close the client
_, exc, _ = sys.exc_info()
if self.client:
self.send_close(exc.args[0], exc.args[1])
except self.EClose:
_, exc, _ = sys.exc_info()
# Connection was not a WebSockets connection
@@ -775,6 +786,8 @@ Sec-WebSocket-Accept: %s\r
self.rec.close()
if self.client and self.client != startsock:
# Close the SSL wrapped socket
# Original socket closed by caller
self.client.close()
def new_client(self):

View File

@@ -190,7 +190,8 @@ Traffic Legend:
if target in ins:
# Receive target data, encode it and queue for client
buf = target.recv(self.buffer_size)
if len(buf) == 0: raise self.EClose("Target closed")
if len(buf) == 0:
raise self.CClose(1000, "Target closed")
cqueue.append(buf)
self.traffic("{")
@@ -210,10 +211,9 @@ Traffic Legend:
if closed:
# TODO: What about blocking on client socket?
self.send_close()
raise self.EClose(closed)
raise self.CClose(closed['code'], closed['reason'])
if __name__ == '__main__':
def websockify_init():
usage = "\n %prog [options]"
usage += " [source_addr:]source_port target_addr:target_port"
usage += "\n %prog [options]"
@@ -280,3 +280,6 @@ if __name__ == '__main__':
# Create and start the WebSockets proxy
server = WebSocketProxy(**opts.__dict__)
server.start_server()
if __name__ == '__main__':
websockify_init()

View File

@@ -84,14 +84,23 @@
}
window.onload = function () {
var host, port, password, path;
var host, port, password, path, token;
$D('sendCtrlAltDelButton').style.display = "inline";
$D('sendCtrlAltDelButton').onclick = sendCtrlAltDel;
document.title = unescape(WebUtil.getQueryVar('title', 'noVNC'));
host = WebUtil.getQueryVar('host', null);
port = WebUtil.getQueryVar('port', null);
// 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 a token variable is passed in, set the parameter in a cookie.
// This is used by nova-novncproxy.
token = WebUtil.getQueryVar('token', null);
if (token) {
WebUtil.createCookie('token', token, 1)
}
password = WebUtil.getQueryVar('password', '');
path = WebUtil.getQueryVar('path', 'websockify');
if ((!host) || (!port)) {