Commit 6628b0e9 authored by Solly Ross's avatar Solly Ross

Enable noVNC to become Browserifiable

This commit restructures noVNC, splitting it into the core directory
and the app directory, with the former containing core noVNC parts,
and the latter containing parts specific to the application.

Additionally, it introduces a new utility for transforming the noVNC
javascript files into a browserifiable application.  Module names and
requirements are listed at the top of each file, and the script
`use_require.js` may be used to convert them into a form that uses
`module.exports` and `require`.  If you pass the '-b' flag to
the script, it will aslo run browserify, and copy over sufficient
resources to use 'vnc.html'.
parent 1138bdd4
......@@ -4,3 +4,4 @@ tests/data_*.js
utils/rebind.so
utils/websockify
node_modules
build
[submodule "include/web-socket-js-project"]
path = include/web-socket-js-project
url = https://github.com/gimite/web-socket-js.git
......@@ -51,12 +51,6 @@ licenses (all MPL 2.0 compatible):
include/jsunzip.js : zlib/libpng license
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
......
......@@ -69,11 +69,7 @@ See more screenshots <a href="http://kanaka.github.com/noVNC/screenshots.html">h
* HTML5 Canvas (with createImageData): Chrome, Firefox 3.6+, iOS
Safari, Opera 11+, Internet Explorer 9+, etc.
* HTML5 WebSockets: For browsers that do not have builtin
WebSockets support, the project includes
<a href="http://github.com/gimite/web-socket-js">web-socket-js</a>,
a WebSockets emulator using Adobe Flash. iOS 4.2+ has built-in
WebSocket support.
* HTML5 WebSockets
* Fast Javascript Engine: this is not strictly a requirement, but
without a fast Javascript engine, noVNC might be painfully slow.
......@@ -130,7 +126,6 @@ use a WebSockets to TCP socket proxy. There is a python proxy included
* 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),
......
This diff is collapsed.
......@@ -34,9 +34,9 @@
<!-- Stylesheets -->
<link rel="stylesheet" href="include/base.css" />
<link rel="alternate stylesheet" href="include/black.css" TITLE="Black" />
<link rel="alternate stylesheet" href="include/blue.css" TITLE="Blue" />
<link rel="stylesheet" href="base.css" />
<link rel="alternate stylesheet" href="black.css" TITLE="Black" />
<link rel="alternate stylesheet" href="blue.css" TITLE="Blue" />
<!--
<script type='text/javascript'
......@@ -212,8 +212,10 @@
</div>
</div>
<script src="include/util.js"></script>
<script src="include/ui.js"></script>
<!-- begin scripts -->
<script src="../core/util.js"></script>
<script src="ui.js"></script>
<!-- end scripts -->
</body>
</html>
......@@ -34,13 +34,13 @@
<!-- Stylesheets -->
<link rel="stylesheet" href="include/base.css" title="plain">
<link rel="stylesheet" href="base.css" title="plain">
<!--
<script type='text/javascript'
src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script>
-->
<script src="include/util.js"></script>
<script src="../core/util.js"></script>
</head>
<body style="margin: 0px;">
......@@ -75,9 +75,11 @@
"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", "keysym.js"]);
Util.load_scripts(
{'../core': ["base64.js", "websock.js", "des.js", "keysymdef.js",
"keyboard.js", "input.js", "display.js", "jsunzip.js",
"rfb.js", "keysym.js"],
'.': ["webutil.js"]});
var rfb;
var resizeTimeout;
......@@ -87,7 +89,7 @@
if (WebUtil.getQueryVar('resize', false)) {
var innerW = window.innerWidth;
var innerH = window.innerHeight;
var controlbarH = $D('noVNC_status_bar').offsetHeight;
var controlbarH = document.getElementById('noVNC_status_bar').offsetHeight;
var padding = 5;
if (innerW !== undefined && innerH !== undefined)
rfb.setDesktopSize(innerW, innerH - controlbarH - padding);
......@@ -104,11 +106,11 @@
msg += 'Password Required: ';
msg += '<input type=password size=10 id="password_input" class="noVNC_status">';
msg += '<\/form>';
$D('noVNC_status_bar').setAttribute("class", "noVNC_status_warn");
$D('noVNC_status').innerHTML = msg;
document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn");
document.getElementById('noVNC_status').innerHTML = msg;
}
function setPassword() {
rfb.sendPassword($D('password_input').value);
rfb.sendPassword(document.getElementById('password_input').value);
return false;
}
function sendCtrlAltDel() {
......@@ -129,9 +131,9 @@
}
function updateState(rfb, state, oldstate, msg) {
var s, sb, cad, level;
s = $D('noVNC_status');
sb = $D('noVNC_status_bar');
cad = $D('sendCtrlAltDelButton');
s = document.getElementById('noVNC_status');
sb = document.getElementById('noVNC_status_bar');
cad = document.getElementById('sendCtrlAltDelButton');
switch (state) {
case 'failed': level = "error"; break;
case 'fatal': level = "error"; break;
......@@ -166,7 +168,7 @@
function xvpInit(ver) {
var xvpbuttons;
xvpbuttons = $D('noVNC_xvp_buttons');
xvpbuttons = document.getElementById('noVNC_xvp_buttons');
if (ver >= 1) {
xvpbuttons.style.display = 'inline';
} else {
......@@ -177,11 +179,11 @@
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;
document.getElementById('sendCtrlAltDelButton').style.display = "inline";
document.getElementById('sendCtrlAltDelButton').onclick = sendCtrlAltDel;
document.getElementById('xvpShutdownButton').onclick = xvpShutdown;
document.getElementById('xvpRebootButton').onclick = xvpReboot;
document.getElementById('xvpResetButton').onclick = xvpReset;
WebUtil.init_logging(WebUtil.getQueryVar('logging', 'warn'));
document.title = unescape(WebUtil.getQueryVar('title', 'noVNC'));
......@@ -217,7 +219,7 @@
}
try {
rfb = new RFB({'target': $D('noVNC_canvas'),
rfb = new RFB({'target': document.getElementById('noVNC_canvas'),
'encrypt': WebUtil.getQueryVar('encrypt',
(window.location.protocol === "https:")),
'repeaterID': WebUtil.getQueryVar('repeaterID', ''),
......
......@@ -11,24 +11,8 @@
/*global Util, window, document */
// Globals defined here
var WebUtil = {}, $D;
/*
* Simple DOM selector by ID
*/
if (!window.$D) {
window.$D = function (id) {
if (document.getElementById) {
return document.getElementById(id);
} else if (document.all) {
return document.all[id];
} else if (document.layers) {
return document.layers[id];
}
return undefined;
};
}
/* [module] name: WebUtil; requires: Util */
var WebUtil = {};
/*
* ------------------------------------------------------
......
......@@ -2,6 +2,8 @@
* 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/. */
/* [module] name: Base64 */
// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js
/*jslint white: false */
......
......@@ -77,7 +77,9 @@
/* jslint white: false */
function DES(passwd) {
/* [module] name: DES */
var DES = function (passwd) {
"use strict";
// Tables, permutations, S-boxes, etc.
......@@ -273,4 +275,4 @@ function DES(passwd) {
setKeys(passwd); // Setup keys
return {'encrypt': encrypt}; // Public interface
} // function DES
}; // function DES
......@@ -10,6 +10,7 @@
/*jslint browser: true, white: false */
/*global Util, Base64, changeCursor */
/* [module] name: Display; requires: Util, Base64 */
var Display;
(function () {
......
......@@ -8,7 +8,8 @@
/*jslint browser: true, white: false */
/*global window, Util */
var Keyboard, Mouse;
/* [module] name: Keyboard; requires: Util, KeyboardUtil */
var Keyboard;
(function () {
"use strict";
......@@ -27,10 +28,10 @@ var Keyboard, Mouse;
});
// create the keyboard handler
this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(),
VerifyCharModifier( /* jshint newcap: false */
TrackKeyState(
EscapeModifiers(this._handleRfbEvent.bind(this))
this._handler = new KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(),
KeyboardUtil.VerifyCharModifier( /* jshint newcap: false */
KeyboardUtil.TrackKeyState(
KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this))
)
)
); /* jshint newcap: true */
......@@ -149,7 +150,12 @@ var Keyboard, Mouse;
//
// Mouse event handler
//
})();
/* [module] name: Mouse; requires: Util */
var Mouse;
(function () {
Mouse = function (defaults) {
this._mouseCaptured = false;
......
/*
* 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
*
......@@ -200,9 +39,11 @@ function JSUnzip() {
* reading more then 8 bits (needed in some zlib streams)
*/
/* [module] name: TINF */
"use strict";
function TINF() {
function TINF () {
this.OK = 0;
this.DATA_ERROR = (-3);
......
This diff is collapsed.
This diff is collapsed.
/* [module] Name: keysyms */
// 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:
......
......@@ -10,8 +10,10 @@
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
*/
/* [module] Name: RFB ; Requires: Util, Display, Keyboard, Mouse, Websock, Base64, DES, KeyTable, TINF */
/*jslint white: false, browser: true */
/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */
/*global window, Util, Display, Keyboard, Mouse, Websock, Base64, DES, KeyTable */
var RFB;
......@@ -213,21 +215,8 @@ var RFB;
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 {
Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version);
if (!Util.Flash || Util.Flash.version < 9) {
this._cleanupSocket('fatal');
throw new Exception("WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash</a> is required");
} else if (document.location.href.substr(0, 7) === 'file://') {
this._cleanupSocket('fatal');
throw new Exception("'file://' URL is incompatible with Adobe Flash");
} else {
this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
}
}
Util.Info("Using native WebSockets");
this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode);
Util.Debug("<< RFB.constructor");
};
......@@ -265,12 +254,12 @@ var RFB;
Util.Info("Sending Ctrl-Alt-Del");
var arr = [];
arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 1));
arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 1));
arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 1));
arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 0));
arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 0));
arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 0));
arr = arr.concat(RFB.messages.keyEvent(KeyTable.XK_Control_L, 1));
arr = arr.concat(RFB.messages.keyEvent(KeyTable.XK_Alt_L, 1));
arr = arr.concat(RFB.messages.keyEvent(KeyTable.XK_Delete, 1));
arr = arr.concat(RFB.messages.keyEvent(KeyTable.XK_Delete, 0));
arr = arr.concat(RFB.messages.keyEvent(KeyTable.XK_Alt_L, 0));
arr = arr.concat(RFB.messages.keyEvent(KeyTable.XK_Control_L, 0));
this._sock.send(arr);
},
......
......@@ -6,6 +6,8 @@
* See README.md for usage and integration instructions.
*/
/* [module] Name: Util */
/* jshint white: false, nonstandard: true */
/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */
......@@ -376,12 +378,12 @@ Util.decodeUTF8 = function (utf8string) {
// 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.get_include_uri = function (root_dir) {
return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI + root_dir + '/' : root_dir + '/';
};
Util._loading_scripts = [];
Util._pending_scripts = [];
Util.load_scripts = function (files) {
Util.load_scripts = function (files_by_dir) {
"use strict";
var head = document.getElementsByTagName('head')[0], script,
ls = Util._loading_scripts, ps = Util._pending_scripts;
......@@ -410,25 +412,32 @@ Util.load_scripts = function (files) {
}
};
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);
var root_dirs = Object.keys(files_by_dir);
for (var d = 0; d < root_dirs.length; d++) {
var root_dir = root_dirs[d];
var files = files_by_dir[root_dir];
for (var f = 0; f < files.length; f++) {
script = document.createElement('script');
script.type = 'text/javascript';
script.src = Util.get_include_uri(root_dir) + 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);
}
ps.push(script);
}
};
......
......@@ -14,40 +14,12 @@
* read binary data off of the receive queue.
*/
/* [module] Name: Websock ; Requires: Util, Base64 */
/*jslint browser: true, bitwise: true */
/*global Util, Base64 */
// Load Flash WebSocket emulator if needed
// To force WebSocket emulator even when native WebSocket available
//window.WEB_SOCKET_FORCE_FLASH = true;
// To enable WebSocket emulator debug:
//window.WEB_SOCKET_DEBUG=1;
if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
Websock_native = true;
} else if (window.MozWebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
Websock_native = true;
window.WebSocket = window.MozWebSocket;
} else {
/* no builtin WebSocket so load web_socket.js */
Websock_native = false;
(function () {
window.WEB_SOCKET_SWF_LOCATION = Util.get_include_uri() +
"web-socket-js/WebSocketMain.swf";
if (Util.Engine.trident) {
Util.Debug("Forcing uncached load of WebSocketMain.swf");
window.WEB_SOCKET_SWF_LOCATION += "?" + Math.random();
}
Util.load_scripts(["web-socket-js/swfobject.js",
"web-socket-js/web_socket.js"]);
})();
}
function Websock() {
var Websock = function () {
"use strict";
this._websocket = null; // WebSocket object
......@@ -65,7 +37,7 @@ function Websock() {
'close': function () {},
'error': function () {}
};
}
};
(function () {
"use strict";
......
/*
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);
This diff is collapsed.
Subproject commit c0855c6caec589c33acc22b6ee5e562287e65f3d
* How to try
Assuming you have Web server (e.g. Apache) running at http://example.com/ .
- Download web_socket.rb from:
http://github.com/gimite/web-socket-ruby/tree/master
- Run sample Web Socket server (echo server) in example.com with: (#1)
$ ruby web-socket-ruby/samples/echo_server.rb example.com 10081
- If your server already provides socket policy file at port 843, modify the file to allow access to port 10081. Otherwise you can skip this step. See below for details.
- Publish the web-socket-js directory with your Web server (e.g. put it in ~/public_html).
- Change ws://localhost:10081 to ws://example.com:10081 in sample.html.
- Open sample.html in your browser.
- After "onopen" is shown, input something, click [Send] and confirm echo back.
#1: First argument of echo_server.rb means that it accepts Web Socket connection from HTML pages in example.com.
* Troubleshooting
If it doesn't work, try these:
1. Try Chrome and Firefox 3.x.
- It doesn't work on Chrome:
-- It's likely an issue of your code or the server. Debug your code as usual e.g. using console.log.
- It works on Chrome but it doesn't work on Firefox:
-- It's likely an issue of web-socket-js specific configuration (e.g. 3 and 4 below).
- It works on both Chrome and Firefox, but it doesn't work on your browser:
-- Check "Supported environment" section below. Your browser may not be supported by web-socket-js.
2. Add this line before your code:
WEB_SOCKET_DEBUG = true;
and use Developer Tools (Chrome/Safari) or Firebug (Firefox) to see if console.log outputs any errors.
3. Make sure you do NOT open your HTML page as local file e.g. file:///.../sample.html. web-socket-js doesn't work on local file. Open it via Web server e.g. http:///.../sample.html.
4. If you are NOT using web-socket-ruby as your WebSocket server, you need to place Flash socket policy file on your server. See "Flash socket policy file" section below for details.
5. Check if sample.html bundled with web-socket-js works.
6. Make sure the port used for WebSocket (10081 in example above) is not blocked by your server/client's firewall.
7. Install debugger version of Flash Player available here to see Flash errors:
http://www.adobe.com/support/flashplayer/downloads.html
* Supported environments
It should work on:
- Google Chrome 4 or later (just uses native implementation)
- Firefox 3.x, Internet Explorer 8 + Flash Player 9 or later
It may or may not work on other browsers such as Safari, Opera or IE 6. Patch for these browsers are appreciated, but I will not work on fixing issues specific to these browsers by myself.
* Flash socket policy file
This implementation uses Flash's socket, which means that your server must provide Flash socket policy file to declare the server accepts connections from Flash.
If you use web-socket-ruby available at
http://github.com/gimite/web-socket-ruby/tree/master
, you don't need anything special, because web-socket-ruby handles Flash socket policy file request. But if you already provide socket policy file at port 843, you need to modify the file to allow access to Web Socket port, because it precedes what web-socket-ruby provides.
If you use other Web Socket server implementation, you need to provide socket policy file yourself. See
http://www.lightsphere.com/dev/articles/flash_socket_policy.html
for details and sample script to run socket policy file server. node.js implementation is available here:
http://github.com/LearnBoost/Socket.IO-node/blob/master/lib/socket.io/transports/flashsocket.js
Actually, it's still better to provide socket policy file at port 843 even if you use web-socket-ruby. Flash always try to connect to port 843 first, so providing the file at port 843 makes startup faster.
* Cookie considerations
Cookie is sent if Web Socket host is the same as the origin of JavaScript. Otherwise it is not sent, because I don't know way to send right Cookie (which is Cookie of the host of Web Socket, I heard).
Note that it's technically possible that client sends arbitrary string as Cookie and any other headers (by modifying this library for example) once you place Flash socket policy file in your server. So don't trust Cookie and other headers if you allow connection from untrusted origin.
* Proxy considerations
The WebSocket spec (http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol) specifies instructions for User Agents to support proxied connections by implementing the HTTP CONNECT method.
The AS3 Socket class doesn't implement this mechanism, which renders it useless for the scenarios where the user trying to open a socket is behind a proxy.
The class RFC2817Socket (by Christian Cantrell) effectively lets us implement this, as long as the proxy settings are known and provided by the interface that instantiates the WebSocket. As such, if you want to support proxied conncetions, you'll have to supply this information to the WebSocket constructor when Flash is being used. One way to go about it would be to ask the user for proxy settings information if the initial connection fails.
* How to host HTML file and SWF file in different domains
By default, HTML file and SWF file must be in the same domain. You can follow steps below to allow hosting them in different domain.
WARNING: If you use the method below, HTML files in ANY domains can send arbitrary TCP data to your WebSocket server, regardless of configuration in Flash socket policy file. Arbitrary TCP data means that they can even fake request headers including Origin and Cookie.
- Unzip WebSocketMainInsecure.zip to extract WebSocketMainInsecure.swf.
- Put WebSocketMainInsecure.swf on your server, instead of WebSocketMain.swf.
- In JavaScript, set WEB_SOCKET_SWF_LOCATION to URL of your WebSocketMainInsecure.swf.
* How to build WebSocketMain.swf
Install Flex 4 SDK:
http://opensource.adobe.com/wiki/display/flexsdk/Download+Flex+4
$ cd flash-src
$ ./build.sh
* License
New BSD License.
This diff is collapsed.
This diff is collapsed.
......@@ -110,18 +110,18 @@ module.exports = function(config) {
files: [
'tests/fake.*.js',
'tests/assertions.js',
'include/util.js', // load first to avoid issues, since methods are called immediately
'core/util.js', // load first to avoid issues, since methods are called immediately
//'../include/*.js',
'include/base64.js',
'include/keysym.js',
'include/keysymdef.js',
'include/keyboard.js',
'include/input.js',
'include/websock.js',
'include/rfb.js',
'include/jsunzip.js',
'include/des.js',
'include/display.js',
'core/base64.js',
'core/keysym.js',
'core/keysymdef.js',
'core/keyboard.js',
'core/input.js',
'core/websock.js',
'core/rfb.js',
'core/jsunzip.js',
'core/des.js',
'core/display.js',
'tests/test.*.js'
],
......
......@@ -66,7 +66,7 @@ if (all_js && !program.autoInject) {
var eol = content.indexOf('\n', ind);
var modules = content.slice(ind, eol).split(/,\s*/);
modules.forEach(function (mod) {
all_modules[get_path('include/', mod) + '.js'] = 1;
all_modules[get_path('core/', mod) + '.js'] = 1;
});
}
......
This diff is collapsed.
This diff is collapsed.
#!/usr/bin/env node
var path = require('path');
var program = require('commander');
var fs = require('fs');
var fse = require('fs-extra');
var esprima = require('esprima');
program
.option('-b, --browserify', 'create a browserify bundled app')
.parse(process.argv);
// the various important paths
var core_path = path.resolve(__dirname, 'core');
var app_path = path.resolve(__dirname, 'app');
var out_dir_base = path.resolve(__dirname, 'build');
var out_dir_core = path.join(out_dir_base, 'novnc');
var out_dir_app = path.join(out_dir_base, 'app');
var out_dir_full = path.resolve(out_dir_base, 'full');
var find_js_files = function (dir) {
var filenames = fs.readdirSync(dir).filter(function (f) { return f.slice(-3) == '.js'; });
return filenames.map(function (f) { return path.resolve(dir, f); });
};
// load the list of files to process
var target_files_core = find_js_files(core_path);
var target_files_app = find_js_files(app_path);
// make sure the output directory exists
fse.ensureDir(out_dir_base);
fse.emptyDirSync(out_dir_core);
fse.emptyDirSync(out_dir_app);
var module_names = {};
var module_info = {};
var output_files = function (cb) {
// actually write the output files
var cnt = 0;
Object.keys(module_info).forEach(function (out_file, ind, keys) {
var info = module_info[out_file];
var contents = info.module;
contents = contents.replace(/\/\* \[as-module\] (.+) \*\//g, '$1');
contents = contents.replace(/\/\* \[begin skip-as-module\] \*\/(.|\n)+\/\* \[end skip-as-module\] \*\//g, '');
if (info.requires) {
var req_strs = info.requires.map(function (val) {
var req_info = module_info[module_names[val]];
var req_base_path = '.';
if (info.namespace !== req_info.namespace) {
req_base_path = path.join('..', req_info.namespace);
}
var req_path = path.join(req_base_path, req_info.filename);
return "var " + val + " = require('./" + req_path + "');";
});
contents = req_strs.join("\n") + contents;
}
contents = info.header + contents + '\nmodule.exports = ' + info.name + ';';
fs.writeFile(out_file, contents, function (err) {
if (err) { throw err; }
console.log("Wrote " + out_file);
cnt++;
if (cnt == keys.length) {
cb();
}
});
});
// write the index.js file for core
var rfb_file = module_info[module_names.RFB].filename;
var index_js_core = "module.exports = require('./" + rfb_file + "');;'";
var core_index_file = path.join(out_dir_core, 'index.js');
fs.writeFile(core_index_file, index_js_core, function (err) {
if (err) { throw err; }
console.log("Wrote " + core_index_file);
});
// write the index.js file for app
var index_js_app = "var UI = require('./ui');";
var app_index_file = path.join(out_dir_app, 'index.js');
fs.writeFile(app_index_file, index_js_app, function (err) {
if (err) { throw err; }
console.log("Wrote " + app_index_file);
});
};
// populate the module info
var load_files = function (target_files, out_dir, ns, cb) {
var cnt = 0;
target_files.forEach(function (file_path) {
fs.readFile(file_path, function (err, contents_raw) {
console.log("Processing '" + file_path + "'");
if (err) { throw err; }
var contents = contents_raw.toString();
var module_parts = contents.split('/* [module] ');
var module_header = module_parts[0];
module_parts = module_parts.slice(1);
module_parts.forEach(function (module_part) {
var info_end = module_part.indexOf('*/');
var info_raw = module_part.slice(0, info_end);
var module_rest = module_part.slice(info_end + 3);
var info_parts = info_raw.split(';');
var info = {};
info_parts.forEach(function (val) {
var val_parts = val.split(':');
info[val_parts[0].trim().toLowerCase()] = val_parts[1].trim();
});
if (info.requires) {
info.requires = info.requires.split(',').map(function (v) { return v.trim(); });
}
//var file_name = path.basename(file_path);
var mod_file_name = info.name.toLowerCase();
var file_name = mod_file_name + '.js';
var out_file = path.resolve(out_dir, file_name);
info.filename = mod_file_name;
info.module = module_rest;
info.header = module_header;
info.namespace = ns;
// set the name in the global list
module_names[info.name] = out_file;
module_info[out_file] = info;
});
cnt++;
if (cnt == target_files.length) {
cb();
}
});
});
};
var make_full_app = function () {
process.chdir(out_dir_base);
fse.emptyDirSync(out_dir_full);
var app_file = path.join(out_dir_full, 'app.js');
var browserify = require('browserify')();
browserify.add(path.join(out_dir_app, 'index.js'));
browserify.bundle().pipe(fs.createWriteStream(app_file));
console.log("Wrote ", app_file);
var src_dir_app = path.join(__dirname, 'app');
fs.readdir(src_dir_app, function (err, files) {
if (err) { throw err; }
files.forEach(function (src_file) {
var src_file_path = path.resolve(src_dir_app, src_file);
var out_file_path = path.resolve(out_dir_full, src_file);
var ext = path.extname(src_file);
if (ext === '.js' || ext === '.html') return;
fse.copy(src_file_path, out_file_path, function (err) {
if (err) { throw err; }
console.log("Copied file(s) from " + src_file_path + " to " + out_file_path);
});
});
});
var src_html_path = path.resolve(src_dir_app, 'vnc.html');
var out_html_path = path.resolve(out_dir_full, 'vnc.html');
fs.readFile(src_html_path, function (err, contents_raw) {
if (err) { throw err; }
var contents = contents_raw.toString();
var start_marker = '<!-- begin scripts -->\n';
var end_marker = '<!-- end scripts -->';
var start_ind = contents.indexOf(start_marker) + start_marker.length;
var end_ind = contents.indexOf(end_marker, start_ind);
contents = contents.slice(0, start_ind) + '<script src="app.js"></script>\n' + contents.slice(end_ind);
fs.writeFile(out_html_path, contents, function (err) {
if (err) { throw err; }
console.log("Wrote " + out_html_path);
});
});
};
load_files(target_files_core, out_dir_core, 'novnc', function () {
load_files(target_files_app, out_dir_app, 'app', output_files.bind(null, function () {
if (program.browserify) {
make_full_app();
} else {
fs.emptyDirSync(out_dir_full);
fs.rmdir(out_dir_full);
}
}));
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment