├── INSTALL ├── LICENSE ├── README ├── README.md ├── bin ├── cli.js ├── disowner.pl ├── init_vendor.sh ├── populate.js ├── robit.js ├── screencasts.js ├── screenshots.js ├── server.js └── thumb.js ├── data └── users.json ├── lib ├── models │ ├── access.js │ ├── contact.js │ ├── disk.js │ ├── fb.js │ ├── fb │ │ ├── encoder.js │ │ └── input.js │ ├── managers.js │ ├── managers │ │ ├── qemu.js │ │ └── vmware.js │ ├── process.js │ └── user.js ├── resources │ └── process.js ├── service.js ├── session.js ├── setup │ ├── config.js │ ├── deploy.js │ ├── mkdir_p.js │ ├── starter.js │ └── user.js ├── web.js └── web │ └── player.js ├── package.json ├── static ├── css │ ├── player │ │ ├── chat.css │ │ ├── fb.css │ │ ├── login.css │ │ ├── sidebar.css │ │ ├── window.css │ │ └── workspace.css │ └── site │ │ └── layout.css ├── img │ ├── buttons │ │ ├── close.png │ │ ├── fullscreen.png │ │ ├── menu-down.png │ │ ├── menu-up.png │ │ └── minimize.png │ ├── default-vm.png │ ├── empty-cursor.cur │ ├── empty-cursor.png │ ├── icons │ │ └── tux.png │ ├── stackvm-200x48.png │ └── stackvm.png └── js │ ├── session.js │ ├── site.js │ ├── ui.js │ ├── ui │ ├── chat.js │ ├── fb.js │ ├── fb │ │ └── display.js │ ├── sidebar.js │ ├── sidebar │ │ └── menustack.js │ ├── taskbar.js │ ├── window.js │ ├── window │ │ └── titlebar.js │ └── workspace.js │ └── util │ ├── events.js │ ├── keymap.js │ └── require.js └── views ├── layout.html ├── pages └── index.html ├── player.html └── userbar.html /INSTALL: -------------------------------------------------------------------------------- 1 | First you'll need node.js. Download, unpack and install it from http://nodejs.org/ 2 | 3 | Next, you'll need npm: 4 | 5 | git clone http://github.com/isaacs/npm.git 6 | cd npm 7 | bash ./scripts/install.sh 8 | 9 | Next, you'll need these node.js modules. They can be installed via npm: 10 | 11 | npm install dnode bufferlist rfb png jpeg gif video base64 12 | 13 | To get them compiles make sure you have libjpeg, libpng, giflib, libtheora and libogg. 14 | 15 | Then get the stackvm itself: 16 | 17 | git clone http://github.com/substack/stackvm.git 18 | 19 | Next get jquery, jquery-ui and jquery-mouswheel plugin [1]. 20 | 21 | Put them in static/js/vendor directory and make sure jquery is named just jquery.js, 22 | and jquery-ui is named jquery-ui.js. 23 | 24 | Then make sure you have qemu and an image you want to run. 25 | 26 | Edit data/users.json to add yourself, and then `mkdir -p users/yournick/disks`. Put the 27 | vm image in that directory. 28 | 29 | Then start manager.js: 30 | 31 | node bin/manager.js 32 | 33 | And then webstack.js: 34 | 35 | node bin/webstack.js 36 | 37 | Now go to http://localhost:9000. 38 | 39 | [1]: http://github.com/brandonaaron/jquery-mousewheel 40 | 41 | --- 42 | 43 | TODO: proper package.json, auto-fetch jquery stuff 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the GNU Affero General Public License version 3 2 | (AGPLv3), or any later version. 3 | See http://www.gnu.org/licenses/agpl-3.0.txt 4 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | StackVM is a web-based virtual machine manager where users will 3 | * interact directly with the graphical or text console 4 | * drop virtual machines into a virtual network diagram 5 | * access files on the virtual machine directly 6 | * create, duplicate, snapshot, and share virtual machine images 7 | * integrate non-virtual servers into the virtual network 8 | 9 | Running: see INSTALL 10 | 11 | http://stackvm.com 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | StackVM 2 | ======= 3 | StackVM is a web-based virtual machine stack for managing, controlling, and 4 | sharing VMS all through a browser. 5 | 6 | Installation 7 | ============ 8 | 9 | Dependencies 10 | ------------ 11 | 12 | * [qemu](http://qemu.org) 13 | * [npm](http://github.com/isaacs/npm) 14 | 15 | Steps 16 | ----- 17 | 18 | git clone http://github.com/substack/stackvm.git 19 | cd stackvm 20 | npm install 21 | 22 | Running 23 | ======= 24 | node server.js 25 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs'); 3 | var sys = require('sys'); 4 | var Hash = require('traverse/hash'); 5 | 6 | var Deployer = require('../lib/setup/deploy'); 7 | var Starter = require('../lib/setup/starter'); 8 | 9 | var argv = require('optimist') 10 | .usage(fs.readFileSync(__dirname + '/../doc/cli.txt', 'utf8')) 11 | .check(function (args) { if (args._.length == 0) throw '' }) 12 | .argv; 13 | 14 | function updateJSON (file, f) { 15 | fs.readFile(file, function (err, buf) { 16 | var data = JSON.parse(buf.toString()); 17 | var out = JSON.stringify(f(data)); 18 | fs.write(file, out); 19 | }); 20 | } 21 | 22 | var cmd = argv._.shift(); 23 | var action = { 24 | deploy : function () { 25 | if (argv._.length < 2) { 26 | throw 'Usage: deploy [name] [directory] {options}'; 27 | } 28 | 29 | runAction('Deploying StackVM to ' + argv._[1], function (cb) { 30 | Deployer.deploy(Hash.merge(argv, { 31 | name : argv._[0], 32 | base : argv._[1], 33 | done : cb, 34 | })); 35 | }); 36 | }, 37 | undeploy : function () { 38 | if (argv._.length == 0) { 39 | throw 'Usage: undeploy [name] {options}'; 40 | } 41 | 42 | runAction('Undeploying StackVM at ' + argv._[0], function (cb) { 43 | Deployer.undeploy(argv._[0], cb); 44 | }); 45 | }, 46 | start : function () { 47 | var name = argv._.length ? argv._[0] : 'main'; 48 | runAction('Starting StackVM:' + name, function (cb) { 49 | Starter.start(name, cb) 50 | }); 51 | }, 52 | stop : function () { 53 | }, 54 | }[cmd]; 55 | 56 | function runAction (msg, cb) { 57 | sys.print(msg + '... '); 58 | cb(function (err) { 59 | if (err) { 60 | console.log('failed'); 61 | console.error('\n !!! ' + (err.stack ? err.stack : err) + '\n'); 62 | } 63 | else console.log('ok') 64 | }); 65 | } 66 | 67 | if (action === undefined) { 68 | console.error('Undefined command ' + sys.inspect(cmd)); 69 | } 70 | else { 71 | try { action() } 72 | catch (err) { 73 | console.error('\n' + (err.stack ? err.stack : err)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /bin/disowner.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use POSIX qw/WNOHANG/; 5 | use Time::HiRes qw/sleep/; 6 | $|++; 7 | 8 | $SIG{CHLD} = 'IGNORE'; 9 | if (my $pid = fork) { 10 | print $pid; 11 | } 12 | else { 13 | sleep 0.1; # time for the print to finish up 14 | close \*STDERR; 15 | close \*STDOUT; 16 | exec @ARGV; 17 | } 18 | -------------------------------------------------------------------------------- /bin/init_vendor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # can call this from cli.js 4 | # 5 | 6 | set -e 7 | 8 | test $# -gt 0 && dst=${1%/} || { 9 | echo "Usage: $0 " 10 | exit 1 11 | } 12 | 13 | mkdir -p "$dst" 14 | 15 | jquery="http://code.jquery.com/jquery-1.4.2.min.js" 16 | jquery_ui="http://jqueryui.com/download/jquery-ui-1.8.5.custom.zip" 17 | jquery_wheel="http://github.com/brandonaaron/jquery-mousewheel.git" 18 | 19 | function jQuery { 20 | wget $jquery -O "$dst/jquery.js" 21 | } 22 | 23 | function jQueryUi { 24 | wget $jquery_ui -O "/tmp/${jquery_ui##*/}" 25 | jquery_ui_dir="/tmp/jquery-ui-$RANDOM" 26 | mkdir $jquery_ui_dir 27 | unzip "/tmp/${jquery_ui##*/}" -d $jquery_ui_dir 28 | cp "$jquery_ui_dir/js/jquery-ui-1.8.5.custom.min.js" "$dst/jquery-ui.js" 29 | rm -rf $jquery_ui_dir 30 | } 31 | 32 | function jQueryMouseWheel { 33 | jquery_wheel_dir="/tmp/jquery-wheel-$RANDOM" 34 | git clone "http://github.com/brandonaaron/jquery-mousewheel.git" $jquery_wheel_dir 35 | cp "$jquery_wheel_dir/jquery.mousewheel.js" "$dst" 36 | rm -rf $jquery_wheel_dir 37 | } 38 | 39 | echo "Installing jQuery" 40 | jQuery >/dev/null 41 | 42 | echo "Installing jQuery-ui" 43 | jQueryUi >/dev/null 44 | 45 | echo "Installing jQuery-mousewheel" 46 | jQueryMouseWheel >/dev/null 47 | 48 | echo "Done" 49 | 50 | -------------------------------------------------------------------------------- /bin/populate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'); 4 | var Store = require('supermarket'); 5 | var Hash = require('traverse/hash'); 6 | 7 | if (process.argv.length <= 2) { 8 | console.error( 9 | 'Usage: %s %s', 10 | process.argv.slice(0,2).join(' '), 11 | '[ json file ]' 12 | ); 13 | process.exit(); 14 | } 15 | 16 | var json = JSON.parse(fs.readFileSync(process.argv[2])); 17 | 18 | Store( 19 | { filename : __dirname + '/../data/users.db', json : true }, 20 | function (err, db) { 21 | Hash(json).forEach(function (user, name) { 22 | db.set(name, user, function (err) { 23 | if (err) throw err; 24 | }); 25 | }); 26 | } 27 | ); 28 | 29 | -------------------------------------------------------------------------------- /bin/robit.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var DNode = require('dnode'); 3 | var Hash = require('traverse/hash'); 4 | 5 | DNode.connect(9090, function (remote) { 6 | remote.authenticate('robit', 'yc', function (err, account) { 7 | if (err) { throw err; return } 8 | 9 | var ycdemo = account.contacts.ycdemo; 10 | 11 | var simpleLinux = account.disks['linux-0.2.img']; 12 | simpleLinux.subscribe(function (sub) { 13 | sub.on('spawn', function (proc) { 14 | ycdemo.subscribe(function (sub) { 15 | sub.on('online', script.bind({}, ycdemo, proc)); 16 | }); 17 | }); 18 | }); 19 | simpleLinux.spawn('qemu'); 20 | }); 21 | }); 22 | 23 | function script (yc, proc) { 24 | console.log('ycdemo is online'); 25 | 26 | var items = [ 27 | [ 2000, function () { 28 | yc.message('Hello! I am the StackVM robot.'); 29 | } ], 30 | [ 2000, function () { 31 | yc.message("I'm here to show off some nifty " 32 | + 'features, like this chat!'); 33 | } ], 34 | [ 2000, function () { 35 | yc.message("Or here, I'll share a VM with you."); 36 | } ], 37 | [ 1500, function () { 38 | yc.share('process', proc.address, { 39 | ycdemo : { input : true, view : true } 40 | }); 41 | } ], 42 | [ 10000, function () { 43 | yc.message('The VM is bound to the framebuffer, ' 44 | + 'so you can `xinit` and everything!'); 45 | } ], 46 | [ 20000, function () { 47 | yc.message('Now try spawning your own VM. ' 48 | + 'Click "qemu" under "disk images" in the left pane.'); 49 | } ], 50 | ]; 51 | 52 | (function next (yc) { 53 | var ix = items.shift(); 54 | if (ix) setTimeout(function () { ix[1](); next() }, ix[0]); 55 | })(); 56 | } 57 | -------------------------------------------------------------------------------- /bin/screencasts.js: -------------------------------------------------------------------------------- 1 | // screencast recording service via dnode 2 | 3 | var DNode = require('dnode').DNode; 4 | var http = require('http'); 5 | var fs = require('fs'); 6 | var sys = require('sys'); 7 | var StackedVideo = require('video').StackedVideo; 8 | var Buffer = require('buffer').Buffer; 9 | 10 | var screencastDir = "./static/screencasts"; 11 | 12 | function randomFileName () { 13 | var fileName = ''; 14 | for (var i = 0; i<32; i++) { 15 | fileName += String.fromCharCode(Math.random()*26 + 97); 16 | } 17 | return fileName; 18 | } 19 | 20 | var activeRecordings = {}; 21 | 22 | DNode({ 23 | startScreencast : function (width, height, f) { 24 | sys.log('start screencast'); 25 | var fileName = randomFileName() + '.ogv'; 26 | var fullPath = screencastDir + '/' + fileName; 27 | var video = new StackedVideo(width, height); 28 | video.setOutputFile(fullPath); 29 | video.setKeyFrameInterval(1024); 30 | activeRecordings[fileName] = video; 31 | f(fileName); 32 | }, 33 | stopScreencast : function (fileName, f) { 34 | sys.log('stop screencast'); 35 | var video = activeRecordings[fileName]; 36 | video.end(); 37 | delete activeRecordings[fileName]; 38 | f({ 39 | status : 'success', 40 | fileName : fileName 41 | }); 42 | }, 43 | newFrame : function (fileName, frame, timeStamp) { 44 | sys.log('new frame: ' + fileName); 45 | var video = activeRecordings[fileName]; 46 | var buf = new Buffer(frame.length); 47 | buf.write(frame, 'binary'); 48 | video.newFrame(buf, timeStamp); 49 | }, 50 | pushUpdate : function (fileName, frame, x, y, w, h) { 51 | sys.log('push update'); 52 | var video = activeRecordings[fileName]; 53 | var buf = new Buffer(frame.length); 54 | buf.write(frame, 'binary'); 55 | video.push(buf, x, y, w, h); 56 | }, 57 | endPush : function (fileName, timeStamp) { 58 | sys.log('end push'); 59 | var video = activeRecordings[fileName]; 60 | video.endPush(timeStamp); 61 | } 62 | }).listen(9300); 63 | 64 | http.createServer(function (req, res) { 65 | if (!/^\/[a-z]{32}\.ogv$/.test(req.url)) { 66 | res.writeHead(400); 67 | res.end(); 68 | return; 69 | } 70 | 71 | var fileName = screencastDir + req.url; 72 | sys.log(fileName); 73 | res.writeHead(200, { 'Content-Type': 'video/ogg' }); 74 | // todo: move this to fs.createReadStream (cause the files for longer screencasts are HUGE) 75 | fs.readFile(fileName, 'binary', function (err, data) { 76 | if (err) { 77 | var msg = "Error reading " + fileName + ": " + err.toString(); 78 | sys.log(msg); 79 | res.write(msg); 80 | res.end(); 81 | return; 82 | } 83 | res.write(data, 'binary'); 84 | res.end(); 85 | }); 86 | }).listen(9301); 87 | 88 | sys.log('Screencast DNode server running on port 9300'); 89 | sys.log('Screencast HTTP serving server running on port 9301'); 90 | 91 | -------------------------------------------------------------------------------- /bin/screenshots.js: -------------------------------------------------------------------------------- 1 | // screenshot service via dnode 2 | 3 | var DNode = require('dnode').DNode; 4 | var http = require('http'); 5 | var fs = require('fs'); 6 | var sys = require('sys'); 7 | 8 | var screenshotDir = "./static/screenshots"; 9 | 10 | function randomFileName () { 11 | var fileName = ''; 12 | for (var i = 0; i<32; i++) { 13 | fileName += String.fromCharCode(Math.random()*26 + 97); 14 | } 15 | return fileName; 16 | } 17 | 18 | DNode({ 19 | screenshot : function (png, f) { 20 | var fileName = randomFileName() + '.png'; 21 | var fullPath = screenshotDir + '/' + fileName; 22 | fs.writeFile(fullPath, png, 'binary', function (err) { 23 | if (err) { 24 | f({ 25 | status : 'error', 26 | message : err.toString() 27 | }); 28 | return; 29 | } 30 | f({ 31 | status : 'success', 32 | fileName : fileName 33 | }); 34 | }); 35 | } 36 | }).listen(9200); 37 | 38 | http.createServer(function (req, res) { 39 | if (!/^\/[a-z]{32}\.png$/.test(req.url)) { 40 | res.writeHead(400); 41 | res.end(); 42 | return; 43 | } 44 | 45 | var fileName = screenshotDir + req.url; 46 | sys.log(fileName); 47 | res.writeHead(200, { 'Content-Type': 'image/png' }); 48 | fs.readFile(fileName, 'binary', function (err, data) { 49 | if (err) { 50 | var msg = "Error reading " + fileName + ": " + err.toString(); 51 | sys.log(msg); 52 | res.write(msg); 53 | res.end(); 54 | return; 55 | } 56 | res.write(data, 'binary'); 57 | res.end(); 58 | }); 59 | }).listen(9201); 60 | 61 | sys.log('Screenshot DNode server running on port 9200'); 62 | sys.log('Screenshot HTTP serving server running on port 9201'); 63 | 64 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var express = require('express'); 4 | var DNode = require('dnode'); 5 | var Hash = require('traverse/hash'); 6 | var Cart = require('cart'); 7 | var fs = require('fs'); 8 | 9 | var User = require('../lib/models/user'); 10 | var Web = require('../lib/web'); 11 | var Service = require('../lib/service'); 12 | 13 | var argv = require('optimist').argv; 14 | var port = parseInt(argv._[0], 10) || 9000; 15 | 16 | var app = express.createServer(); 17 | app.use(express.staticProvider(__dirname + '/../static')); 18 | app.use(express.cookieDecoder()); 19 | app.use(express.bodyDecoder()); 20 | app.use(express.session({ 21 | store : new Cart({ dbFile : process.cwd() + '/data/sessions.db' }), 22 | secret : 'todo: set this in the stackvm site config with cli.js' 23 | })); 24 | 25 | app.configure('development', function () { 26 | app.use(express.errorHandler({ 27 | dumpExceptions: true, 28 | showStack: true 29 | })); 30 | }); 31 | 32 | app.configure('production', function () { 33 | app.use(express.errorHandler()); 34 | }); 35 | 36 | app.get('/js/dnode.js', require('dnode/web').route()); 37 | 38 | // TODO: use supermarket here 39 | var users = User.fromHashes( 40 | JSON.parse(fs.readFileSync(process.cwd() + '/data/users.json')) 41 | ); 42 | 43 | Web(app, users); 44 | Service(users, function (service) { 45 | DNode(service) 46 | .listen(app, { 47 | ping : 3600*1000, 48 | timeout : 1000, 49 | transports : 'websocket xhr-multipart xhr-polling htmlfile' 50 | .split(/\s+/), 51 | }) 52 | .listen(9090) 53 | ; 54 | console.log('StackVM running at http://localhost:' + port); 55 | }); 56 | 57 | app.listen(port, '0.0.0.0'); 58 | -------------------------------------------------------------------------------- /bin/thumb.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var DNode = require('dnode'); 3 | var Hash = require('traverse/hash'); 4 | 5 | DNode.connect(9090, function (remote, conn) { 6 | remote.authenticate('robit', 'yc', function (err, account) { 7 | if (err) { throw err; return } 8 | 9 | var simpleLinux = account.disks['linux-0.2.img']; 10 | simpleLinux.spawn('qemu', function (proc) { 11 | proc.subscribe(function (sub) { 12 | sub.on('thumb', function (filename) { 13 | console.log(simpleLinux.filename + '/' + filename); 14 | }); 15 | }); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /data/users.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "ycdemo" : { 4 | "hash" : "", 5 | "contacts" : [ "robit" ], 6 | "disks" : { 7 | "linux-0.2.img" : { 8 | "name" : "simple linux", 9 | "rules" : {} 10 | }, 11 | "dsl-4.4.10.iso" : { 12 | "name" : "damn small linux", 13 | "rules" : {} 14 | }, 15 | "ReactOS-LiveCD.iso" : { 16 | "name" : "reactos", 17 | "rules": {} 18 | } 19 | } 20 | }, 21 | "robit" : { 22 | "hash" : "2cef66e485a1601325e91c252e2c3702659200a80a096229929dc9d2e1e93978a162f7a07863b0e78a0c2c7da421143ae41c9d72ea7e85a4cca9c7fb65d736ed", 23 | "contacts" : [ "ycdemo" ], 24 | "disks" : { 25 | "linux-0.2.img" : { 26 | "name" : "simple linux", 27 | "rules" : { "moo" : { "spawn" : true } } 28 | } 29 | } 30 | }, 31 | "substack" : { 32 | "hash" : "dce1198b941cdda02f092c9702ee8d0a3efeb89aec50f95bfd31b778a2109cfcb0979fbbec6df4d27ff0da9bc9efb4806ab563bfd4e960ba5a5f2b2fe683e273", 33 | "contacts" : ["moo", "pkrumins", "ik"], 34 | "admin" : true, 35 | "disks" : { 36 | "linux-0.2.img" : { 37 | "name" : "simple linux", 38 | "rules" : { "moo" : { "spawn" : true } } 39 | }, 40 | "ReactOS-LiveCD.iso" : { 41 | "name" : "ReactOS", 42 | "rules" : { "anonymous" : { "spawn" : true } } 43 | }, 44 | "WinXP" : { 45 | "name" : "Windows XP", 46 | "host" : "192.168.1.3:5900" 47 | } 48 | } 49 | }, 50 | "moo" : { 51 | "hash" : "0224ebe0490f6917c2ab4a632367e42dd049bf89770f9be2ff69b77e8f8be9e67ee332dc22ce6d86e0f4218edb93dfe3d2775baa9c6378f88e7398c1d84d4309", 52 | "contacts" : ["substack"], 53 | "disks" : {} 54 | }, 55 | "pkrumins" : { 56 | "hash" : "119a7a63f6bda3c96eeff52cf0376b2a0199753aa2da144bf313aeeded199f708d8a108948aa06c3ff94e468e842de1e0c0f3498b994be1bb1711e2efb65a647", 57 | "disks" : { 58 | "linux-0.2.img" : { 59 | "name" : "simple linux" 60 | }, 61 | "linux-0.2.img-vmware" : { 62 | "name" : "simple linux @ vmware", 63 | "host" : "192.168.1.3:5902" 64 | }, 65 | "WinXP" : { 66 | "name" : "Windows XP", 67 | "host" : "192.168.1.3:5900" 68 | } 69 | }, 70 | "contacts" : ["moo", "substack", "ik"] 71 | }, 72 | "ik" : { 73 | "hash" : "0224ebe0490f6917c2ab4a632367e42dd049bf89770f9be2ff69b77e8f8be9e67ee332dc22ce6d86e0f4218edb93dfe3d2775baa9c6378f88e7398c1d84d4309", 74 | "disks" : { 75 | "linux-0.2.img" : { 76 | "name" : "simple linux" 77 | } 78 | }, 79 | "contacts" : ["pkrumins", "substack"] 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /lib/models/access.js: -------------------------------------------------------------------------------- 1 | var Hash = require('traverse/hash'); 2 | 3 | module.exports = function (rules) { 4 | var self = {}; 5 | 6 | self.allowed = function (user) { 7 | var can = Hash(rules).reduce(function (mask, rule, u) { 8 | return u == user.name ? Hash(mask).merge(rule).items : mask 9 | }, rules.anonymous || {}); 10 | return Hash(can).length ? can : undefined; 11 | }; 12 | 13 | return self; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/models/contact.js: -------------------------------------------------------------------------------- 1 | // Route contact events 2 | var RemoteEmitter = require('dnode/events'); 3 | var Access = require('./access'); 4 | 5 | module.exports = Contact; 6 | Contact.prototype = new RemoteEmitter; 7 | 8 | function Contact (from, to) { 9 | if (!(this instanceof Contact)) return new Contact(from, to); 10 | var self = this; 11 | 12 | self.name = to.name; 13 | 14 | self.online = to.connections > 0; 15 | 16 | to.on('online', function () { 17 | self.online = true; 18 | self.emit('online'); 19 | }); 20 | 21 | to.on('offline', function () { 22 | self.online = false; 23 | self.emit('offline'); 24 | }); 25 | 26 | self.message = function (msg) { 27 | if (!self.online) throw new Error( 28 | "Tried to message " + self.name + ", who is offline" 29 | ) 30 | to.contacts[from.name].emit('message', msg); 31 | }; 32 | 33 | self.share = function (type, id, rules) { 34 | var res = { 35 | process : function () { return from.processAt(id) }, 36 | disk : function () { return from.disks[id] }, 37 | }[type](); 38 | if (!res) throw new Error('No ' + type + ' at ' + id); 39 | 40 | to.share(res, function (tied) { 41 | var limited = tied.limit(to, Access(rules)); 42 | if (limited) to.contacts[from.name].emit('share', type, limited); 43 | }); 44 | }; 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /lib/models/disk.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var RemoteEmitter = require('dnode/events'); 3 | var Hash = require('traverse/hash'); 4 | 5 | var managers = require('./managers'); 6 | var Access = require('./access'); 7 | 8 | module.exports = Disk; 9 | Disk.prototype = new RemoteEmitter; 10 | function Disk (params) { 11 | if (!(this instanceof Disk)) return new Disk(params); 12 | var self = this; 13 | 14 | // DNode clients can't see or call stuff in __proto__. 15 | self.__proto__ = Hash.copy(self.__proto__); 16 | 17 | self.name = params.name; 18 | self.processes = {}; 19 | var filename = params.filename; 20 | self.filename = path.basename(filename); // client sees just the basename 21 | 22 | function addProcess (proc) { 23 | self.processes[proc.address] = proc; 24 | proc.on('exit', function () { 25 | delete self.processes[proc.address]; 26 | }); 27 | } 28 | 29 | Hash(managers).forEach(function (manager) { 30 | Hash(manager.processes).forEach(function (proc) { 31 | if (proc.filename == self.filename) { 32 | addProcess(proc); 33 | } 34 | }); 35 | }); 36 | 37 | self.__proto__.access = Access(params.rules || {}); 38 | 39 | self.__proto__.limit = function (user, access) { 40 | var can = (access ? access : self.access).allowed(user); 41 | 42 | var share = Hash.copy(this); 43 | if (!can.copy) delete share.copy; 44 | if (!can.spawn) delete share.spawn; 45 | 46 | share.processes = Hash(share.processes) 47 | .map(function (proc) { 48 | var pcan = proc.access.allowed(user); 49 | if (pcan) return proc.limit(user); 50 | }) 51 | .filter(function (x) { return x !== undefined }) 52 | .items 53 | ; 54 | 55 | return share; 56 | }; 57 | 58 | self.on('attach', function (tied) { 59 | tied.tie('processes'); 60 | 61 | // spawn a new vm process 62 | tied.spawn = function (engine, cb) { 63 | managers[engine].spawn(filename, function (proc) { 64 | proc.name = self.name; 65 | 66 | addProcess(proc); 67 | var tProc = tied.tie(proc); 68 | 69 | if (cb) cb(tProc); 70 | self.emit('spawn', tProc); 71 | }); 72 | }; 73 | 74 | tied.copy = function (dstFile) { 75 | console.log('copy not implemented... yet'); 76 | }; 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /lib/models/fb.js: -------------------------------------------------------------------------------- 1 | // Connect to an RFB service, exposing an input to send (keyboard, mouse) to and 2 | // an encoder to grab framebuffer updates from. 3 | 4 | var RFB = require('rfb'); 5 | var Input = require('./fb/input'); 6 | var Encoder = require('./fb/encoder'); 7 | 8 | module.exports = function FB (addr, engine, cb) { 9 | var rfb = new RFB({ 10 | host : addr.split(/:/)[0], 11 | port : addr.split(/:/)[1], 12 | engine : engine, 13 | }); 14 | 15 | var fb = { 16 | input : new Input(rfb), 17 | encoder : new Encoder(rfb), 18 | size : null, 19 | end : function () { 20 | fb.encoder.emit('end'); 21 | rfb.end(); // rfb should probably emit an event itself, but meh 22 | }, 23 | }; 24 | 25 | fb.encoder.on('desktopSize', function (size) { 26 | fb.size = size; 27 | }); 28 | 29 | rfb.on('error', cb); 30 | 31 | fb.encoder.dimensions(function (size) { 32 | rfb.removeListener('error', cb); 33 | fb.size = size; 34 | FB[addr] = fb; 35 | cb(null, fb); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/models/fb/encoder.js: -------------------------------------------------------------------------------- 1 | /* Emit png (and soon jpeg) tiles from an RFB stream. 2 | Emits these events: 3 | error, screenUpdate, copyRect, desktopSize 4 | */ 5 | 6 | var sys = require('sys'); 7 | var RemoteEmitter = require('dnode/events'); 8 | var PngLib = require('png'); 9 | var Buffer = require('buffer').Buffer; 10 | var base64 = require('base64'); 11 | 12 | module.exports = Encoder; 13 | Encoder.prototype = new RemoteEmitter; 14 | function Encoder (rfb) { 15 | if (!(this instanceof Encoder)) return new Encoder(rfb); 16 | var self = this; 17 | var imageStack = null; 18 | var imageType = 'png'; 19 | var bufType = 'bgr'; 20 | 21 | // Request a new complete framebuffer from the rfb server. 22 | self.requestRedraw = function () { 23 | rfb.requestRedraw(); 24 | }; 25 | 26 | // Poll the rfb's dimensions. 27 | // The callback returns a hash with width and height. 28 | self.dimensions = rfb.dimensions; 29 | 30 | // pass some events through directly to consumers 31 | 'end error'.split(/\s+/).forEach(function (ev) { 32 | rfb.on(ev, function () { 33 | var args = [ev].concat([].slice.call(arguments)); 34 | self.emit.apply(self,args); 35 | }); 36 | }); 37 | 38 | rfb.on('desktopSize', function (rect) { 39 | self.emit('desktopSize', { width : rect.width, height : rect.height }); 40 | }); 41 | 42 | rfb.on('copyRect', function (rect) { 43 | self.emit('copyRect', { 44 | srcX : rect.srcX, 45 | srcY : rect.srcY, 46 | width : rect.width, 47 | height : rect.height, 48 | x : rect.x, 49 | y : rect.y 50 | }); 51 | }); 52 | 53 | rfb.on('startRects', function (nRects) { 54 | if (nRects > 1) { 55 | imageStack = new PngLib.DynamicPngStack(bufType); 56 | } 57 | }); 58 | 59 | rfb.on('endRects', function (nRects) { 60 | if (nRects > 1) { 61 | var image = imageStack.encodeSync(); 62 | var dims = imageStack.dimensions(); 63 | 64 | self.emit('screenUpdate', { 65 | base64 : base64.encode(image), 66 | type : imageType, 67 | width : dims.width, 68 | height : dims.height, 69 | x : dims.x, 70 | y : dims.y 71 | }); 72 | } 73 | }); 74 | 75 | rfb.on('raw', function (rect) { 76 | self.emit('raw', rect); 77 | if (rect.nRects == 1) { 78 | rfb.dimensions(function (dims) { 79 | var image = new PngLib.Png(rect.fb, rect.width, rect.height, bufType).encodeSync(); 80 | var fullScreen = (rect.width == dims.width) && 81 | (rect.height == dims.height); 82 | self.emit('screenUpdate', { 83 | base64 : base64.encode(image), 84 | type : imageType, 85 | width : rect.width, 86 | height : rect.height, 87 | x : rect.x, 88 | y : rect.y, 89 | fullScreen : fullScreen 90 | }); 91 | }); 92 | } 93 | else { 94 | imageStack.push(rect.fb, rect.x, rect.y, rect.width, rect.height); 95 | } 96 | }); 97 | 98 | rfb.on('unknownRect', function (rect) { 99 | console.log('received an unknownRect from rfb: ' + sys.inspect(rect)); 100 | self.emit('error', 'received an unknownRect from rfb'); 101 | }); 102 | } 103 | 104 | -------------------------------------------------------------------------------- /lib/models/fb/input.js: -------------------------------------------------------------------------------- 1 | // Wrap around the encoder and rfb events so that multiple dnode clients can 2 | // manage events without interference. 3 | 4 | module.exports = function Input (rfb) { 5 | this.sendKeyDown = function () { 6 | rfb.sendKeyDown.apply(rfb, [].slice.call(arguments)); 7 | }; 8 | 9 | this.sendKeyUp = function () { 10 | rfb.sendKeyUp.apply(rfb, [].slice.call(arguments)); 11 | }; 12 | 13 | this.sendPointer = function () { 14 | rfb.sendPointer.apply(rfb, [].slice.call(arguments)); 15 | }; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /lib/models/managers.js: -------------------------------------------------------------------------------- 1 | var Hash = require('traverse/hash'); 2 | var Store = require('supermarket'); 3 | 4 | module.exports = { 5 | qemu : require('./managers/qemu')(), 6 | vmware : require('./managers/vmware')(), 7 | }; 8 | 9 | Store({ filename : __dirname + '/../../data/procs.db', json : true }, function (err, procs) { 10 | if (err) throw err; 11 | 12 | Hash(module.exports).forEach(function (manager) { 13 | manager.on('spawn', function (proc) { 14 | procs.set( 15 | proc.address, 16 | Hash.extract(proc, 'address engine filename pid'.split(' ')), 17 | function (err) { if (err) throw err } 18 | ); 19 | }); 20 | 21 | manager.on('connect', function (proc) { 22 | proc.on('exit', function () { 23 | procs.remove( 24 | proc.address, 25 | function (err) { if (err) throw err } 26 | ); 27 | }); 28 | }); 29 | }); 30 | 31 | procs.forEach(function (proc) { 32 | console.log('Connecting process ' + proc.value.pid + ' at ' + proc.value.address); 33 | var p = module.exports[proc.value.engine].connect(proc); 34 | if (!p) { 35 | console.log('Removing proccess ' + proc.value.pid + ' at ' + proc.value.address); 36 | procs.remove( 37 | proc.value.address, 38 | function (err) { if (err) throw err } 39 | ); 40 | } 41 | }); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /lib/models/managers/qemu.js: -------------------------------------------------------------------------------- 1 | var child = require('child_process'); 2 | var path = require('path'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var Process = require('../process'); 5 | var Hash = require('traverse/hash'); 6 | 7 | var ports = []; 8 | 9 | module.exports = Qemu; 10 | Qemu.prototype = new EventEmitter; 11 | function Qemu () { 12 | var self = this; 13 | if (!(self instanceof Qemu)) return new Qemu(); 14 | 15 | self.processes = {}; 16 | 17 | self.connect = function (p) { 18 | try { process.kill(p.pid, 0) } 19 | catch (err) { return null } 20 | 21 | var port = parseInt(p.address.split(/:/)[1], 10); 22 | ports.push(port); 23 | 24 | var proc = Process(Hash.merge(p, { 25 | engine : 'qemu', 26 | kill : function (cb) { 27 | try { 28 | process.kill(proc.pid); 29 | if (cb) cb(true); 30 | } 31 | catch (err) { 32 | if (cb) cb(false); 33 | } 34 | } 35 | })); 36 | 37 | self.processes[proc.address] = proc; 38 | self.emit('connect', proc); 39 | return proc; 40 | }; 41 | 42 | self.spawn = function (filename, cb) { 43 | var port = 5900; 44 | for (; ports.indexOf(port) >= 0; port++); 45 | var addr = 'localhost:' + port; // for now 46 | 47 | console.log('firing up qemu on ' + addr); 48 | 49 | var disowner = __dirname + '/../../../bin/disowner.pl'; 50 | var qemu = child.spawn(disowner, [ 51 | 'qemu', '-vnc', ':' + (port - 5900), 52 | filename.match(/\.iso$/) ? '-cdrom' : '-hda', filename 53 | ]); 54 | qemu.stdout.on('data', function (buf) { 55 | var proc = self.connect({ 56 | filename : path.basename(filename), 57 | userdir : path.dirname(filename) + '/..', 58 | address : addr, 59 | pid : parseInt(buf.toString(), 10), 60 | }); 61 | self.emit('spawn', proc); 62 | 63 | proc.on('exit', function () { 64 | console.log('qemu on ' + addr + ' died'); 65 | var i = ports.indexOf(port); 66 | if (i >= 0) ports.splice(i, 1); 67 | delete self.processes[addr]; 68 | }); 69 | 70 | proc.on('ready', function () { cb(proc) }); 71 | }); 72 | }; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /lib/models/managers/vmware.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var net = require('net'); 3 | 4 | module.exports = VMWare; 5 | VMWare.prototype = new EventEmitter; 6 | function VMWare () { 7 | var self = this; 8 | if (!(self instanceof VMWare)) return new VMWare(); 9 | if (!self.processes) self.processes = []; 10 | 11 | self.isAlive = function (proc, f) { 12 | var host = proc.addr.split(':')[0]; 13 | var port = proc.addr.split(':')[1]; 14 | 15 | var stream = net.createConnection(port, host); 16 | stream.setTimeout(1000); 17 | stream.on('connect', function () { 18 | stream.end(); 19 | f(true); 20 | }); 21 | stream.on('error', function () { 22 | f(false); 23 | }); 24 | }; 25 | 26 | self.spawn = function (params, f) { 27 | var proc = new EventEmitter; 28 | proc.disk = params.file; 29 | proc.engine = 'vmware'; 30 | proc.user = params.user; 31 | proc.addr = params.disk.host; 32 | proc.pid = 0; 33 | 34 | self.processes[params.disk.host] = proc; 35 | if (f) f(proc); 36 | }; 37 | 38 | self.kill = function (proc, f) { 39 | delete self.processes[proc.addr]; 40 | if (f) f(true); 41 | }; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /lib/models/process.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var sys = require('sys'); 3 | var path = require('path'); 4 | 5 | var RemoteEmitter = require('dnode/events'); 6 | var Hash = require('traverse/hash'); 7 | var Png = require('png').Png; 8 | var base64 = require('base64'); 9 | 10 | var Access = require('./access'); 11 | var FB = require('./fb'); 12 | 13 | module.exports = Process; 14 | Process.prototype = new RemoteEmitter; 15 | function Process (params) { 16 | if (!(this instanceof Process)) return new Process(params); 17 | var self = this; 18 | self.__proto__ = Hash.copy(self.__proto__); 19 | 20 | self.engine = p.engine; 21 | 22 | Hash.update(self, Hash.extract( 23 | params, 'engine kill filename address pid'.split(' ') 24 | )); 25 | 26 | var thumbDir = params.userdir + '/thumbs/' + self.filename; 27 | path.exists(thumbDir, function (exists) { 28 | if (!exists) fs.mkdir(thumbDir, 0755); 29 | }); 30 | 31 | function saveRect (filename, rect) { 32 | fs.createWriteStream(filename) 33 | .write(rect.base64, 'base64'); 34 | } 35 | 36 | var framebuffer = null; 37 | 38 | self.on('attach', function (tied) { 39 | tied.fb = { 40 | input : framebuffer.input, 41 | encoder : tied.tie(framebuffer.encoder), 42 | size : framebuffer.size, 43 | }; 44 | }); 45 | 46 | setTimeout(function () { // lame hack, give it time to connect 47 | FB(self.address, self.engine, function (err, fb) { 48 | if (err) throw err; 49 | 50 | var hasUpdates = false; 51 | var iv = setInterval((function f () { 52 | if (hasUpdates && self.connections > 0) { 53 | fb.encoder.requestRedraw(); 54 | hasUpdates = false; 55 | } 56 | return f; 57 | })(), 3000); 58 | 59 | fb.encoder.on('end', function () { 60 | clearInterval(iv); 61 | self.emit('exit'); 62 | }); 63 | 64 | fb.encoder.on('screenUpdate', function (rect) { 65 | if (rect.fullScreen && self.connections > 0) { 66 | var file = thumbDir + '/' + self.address + '.png'; 67 | saveRect(file, rect); 68 | self.emit('thumb', rect.width, rect.height); 69 | } 70 | else { 71 | hasUpdates = true; 72 | } 73 | }); 74 | 75 | framebuffer = fb; 76 | self.emit('ready'); 77 | }); 78 | }, 500); 79 | 80 | self.__proto__.access = Access(params.rules || {}); 81 | 82 | self.__proto__.limit = function (user, access) { 83 | var can = (access ? access : self.access).allowed(user); 84 | 85 | var share = Hash.copy(this); 86 | share.fb = Hash.copy(share.fb); 87 | 88 | if (!can.input) delete share.fb.input; 89 | if (!can.view) { 90 | delete share.fb.encoder; 91 | delete share.subscribe; 92 | } 93 | if (!can.kill) delete share.kill; 94 | 95 | return share; 96 | }; 97 | 98 | // probably this will need tied to notify the client 99 | self.screenshot = function () { 100 | FB(self.address, self.engine, function (err, fb) { 101 | fb.encoder.requestRedrawScreen(); 102 | fb.encoder.once('raw', function g (rect) { 103 | fb.encoder.dimensions(function (dims) { 104 | var fullScreen = rect.x == 0 && rect.y == 0 && 105 | rect.width == dims.width && rect.height == dims.height; 106 | if (!fullScreen) { 107 | fb.encoder.once('raw', g); 108 | } 109 | else { 110 | var png = new Png(rect.fb, rect.width, rect.height, 'bgr'). 111 | encode(function (image) { 112 | var fileName = randomFileName(); 113 | var fullPath = __dirname + '/../../static/screenshots/' + 114 | fileName; 115 | 116 | fs.writeFile(fullPath, image, 'binary', 117 | function (err) { 118 | if (err) { 119 | console.log('failed writing screenshot: ' + err); 120 | } 121 | else { 122 | self.emit('screenshot', 123 | 'http://10.1.1.2:9000/screenshots/' + fileName); 124 | } 125 | }); 126 | }); 127 | } 128 | }); 129 | }); 130 | }); 131 | }; 132 | } 133 | 134 | // this shouldn't be here. todo: refactor it all out to screenshots.js 135 | function randomFileName () { 136 | var fileName = ''; 137 | for (var i = 0; i<32; i++) { 138 | fileName += String.fromCharCode(Math.random()*26 + 97); 139 | } 140 | return fileName; 141 | } 142 | 143 | -------------------------------------------------------------------------------- /lib/models/user.js: -------------------------------------------------------------------------------- 1 | // Never give the user direct access to this object, use session instead 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var crypto = require('crypto'); 5 | var Hash = require('traverse/hash'); 6 | 7 | var Disk = require('./disk'); 8 | var Contact = require('./contact'); 9 | 10 | // all manners of hashes in this file 11 | 12 | module.exports = User; 13 | 14 | User.prototype = new EventEmitter; 15 | function User (params) { 16 | if (!(this instanceof User)) return new User(params); 17 | var self = this; 18 | 19 | self.sessions = {}; 20 | self.everyone = {}; 21 | self.connections = 0; 22 | 23 | self.name = params.name; 24 | self.hash = params.hash; 25 | self.admin = params.admin; 26 | 27 | self.directories = { 28 | disks : __dirname + '/../../users/' + self.name + '/disks/', 29 | }; 30 | 31 | self.disks = Hash.map(params.disks, function (disk, filename) { 32 | disk.filename = self.directories.disks + filename; 33 | return new Disk(disk); 34 | }); 35 | 36 | self.processAt = function (addr) { 37 | return Hash(self.disks) 38 | .filter(function (disk) { return disk.processes[addr] }) 39 | .map(function (disk) { return disk.processes[addr] }) 40 | .values[0]; 41 | }; 42 | 43 | self.share = function (res, cb) { 44 | Hash(self.sessions).forEach(function (session) { 45 | cb(res.attach(session.connection)); 46 | }); 47 | }; 48 | } 49 | 50 | User.hash = function (phrase) { 51 | return new crypto.Hash('sha512').update(phrase).digest('hex'); 52 | }; 53 | 54 | User.fromHashes = function (hashes) { 55 | var users = Hash.map(hashes, function (user, name) { 56 | user.name = name; 57 | return new User(user); 58 | }); 59 | 60 | Hash(users).forEach(function (user, name) { 61 | user.contacts = {}; 62 | hashes[name].contacts.forEach(function (name) { 63 | user.contacts[name] = new Contact(user, users[name]); 64 | }); 65 | user.everyone = users; 66 | }); 67 | 68 | return users; 69 | }; 70 | -------------------------------------------------------------------------------- /lib/resources/process.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var FB = require('./fb'); 3 | var Png = require('png').Png; 4 | var fs = require('fs'); 5 | 6 | module.exports = Process; 7 | Process.prototype = new EventEmitter; 8 | function Process (params) { 9 | if (!(this instanceof Process)) return new Process(params); 10 | var self = this; 11 | 12 | self.addr = params.proc.addr; 13 | self.engine = params.proc.engine; 14 | self.disk = params.proc.disk; 15 | self.pid = params.proc.pid; 16 | self.name = params.name; 17 | 18 | params.proc.on('exit', function () { self.emit('exit', self.addr) }); 19 | 20 | self.attach = function (cb) { 21 | FB({ addr : self.addr, engine : self.engine }, function (fb) { 22 | cb({ 23 | input : fb.input, 24 | encoder : fb.encoder, 25 | size : fb.size, 26 | }); 27 | }); 28 | }; 29 | 30 | self.kill = function () { 31 | if (self.addr in FB) FB[self.addr].end(); 32 | self.emit('kill'); 33 | }; 34 | 35 | self.screenshot = function () { 36 | FB({ addr : self.addr, engine : self.engine }, function (fb) { 37 | fb.encoder.requestRedraw(); 38 | fb.encoder.once('raw', function g (rect) { 39 | fb.encoder.dimensions(function (dims) { 40 | var fullScreen = rect.x == 0 && rect.y == 0 && 41 | rect.width == dims.width && rect.height == dims.height; 42 | if (!fullScreen) { 43 | fb.encoder.once('raw', g); 44 | } 45 | else { 46 | var png = new Png(rect.fb, rect.width, rect.height, 'bgr'). 47 | encode(function (image) { 48 | var fileName = randomFileName(); 49 | var fullPath = __dirname + '/../../static/screenshots/' + 50 | fileName; 51 | 52 | fs.writeFile(fullPath, image, 'binary', 53 | function (err) { 54 | if (err) { 55 | console.log('failed writing screenshot: ' + err); 56 | } 57 | else { 58 | self.emit('screenshot', 59 | 'http://10.1.1.2:9000/screenshots/' + fileName); 60 | } 61 | }); 62 | }); 63 | } 64 | }); 65 | }); 66 | }); 67 | }; 68 | } 69 | 70 | // this shouldn't be here. todo: refactor it all out to screenshots.js 71 | function randomFileName () { 72 | var fileName = ''; 73 | for (var i = 0; i<32; i++) { 74 | fileName += String.fromCharCode(Math.random()*26 + 97); 75 | } 76 | return fileName; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | var Hash = require('traverse/hash'); 2 | var qs = require('querystring'); 3 | var Store = require('supermarket'); 4 | 5 | var User = require('./models/user'); 6 | var Session = require('./session'); 7 | 8 | var sessions = Store({ 9 | filename : process.cwd() + '/data/sessions.db', 10 | json : true 11 | }); 12 | 13 | module.exports = function (users, cb) { 14 | cb(function (client, conn) { 15 | var self = this; 16 | 17 | self.session = function (f) { 18 | var cookie = conn.stream.socketio.request.headers.cookie; 19 | var sid = qs.parse(cookie).connect.sid; 20 | if (!sid) f('Not authenticated'); 21 | else fromSid(users, conn, sid, f); 22 | }; 23 | 24 | self.authenticate = function (name, pass, f) { 25 | var user = users[name]; 26 | if (!user || User.hash(pass) != user.hash) { 27 | f('Invalid'); 28 | } 29 | else { 30 | fromUser(user, conn, f); 31 | } 32 | }; 33 | }); 34 | }; 35 | 36 | function fromSid (users, conn, sid, cb) { 37 | sessions.get(sid, function (err, s) { 38 | if (err) { cb(err); return } 39 | 40 | if (s && s.name) { 41 | var user = users[s.name]; 42 | if (!Hash(users).has(s.name)) { cb('Invalid'); return } 43 | 44 | fromUser(user, conn, cb); 45 | } 46 | else { 47 | cb('Not logged in'); 48 | } 49 | }); 50 | } 51 | 52 | function fromUser (user, conn, cb) { 53 | var session = new Session(user, conn); 54 | user.sessions[conn.id] = session; 55 | 56 | if (user.connections == 0) user.emit('online'); 57 | user.connections ++; 58 | 59 | conn.on('end', function () { 60 | user.connections --; 61 | delete user.sessions[conn.id]; 62 | if (user.connections == 0) user.emit('offline'); 63 | }); 64 | 65 | cb(null, session.attach(conn)); 66 | } 67 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | var RemoteEmitter = require('dnode/events'); 2 | var Hash = require('traverse/hash'); 3 | 4 | module.exports = Session; 5 | Session.prototype = new RemoteEmitter; 6 | function Session (user, conn) { 7 | if (!(this instanceof Session)) return new Session(params); 8 | var self = this; 9 | self.__proto__ = Hash.copy(self.__proto__); 10 | self.__proto__.connection = conn; 11 | 12 | self.name = user.name; 13 | self.disks = user.disks; 14 | self.contacts = user.contacts; 15 | 16 | self.on('attach', function (tied) { 17 | tied.tie('disks'); 18 | tied.tie('contacts'); 19 | 20 | tied.browse = function (name, cb) { 21 | cb(Hash(user.everyone[name].disks) 22 | .map(function (disk) { 23 | var can = disk.access.allowed(user); 24 | if (can) return tied.tie(disk).limit(user); 25 | }) 26 | .filter(function (x) { return x !== undefined }) 27 | .items 28 | ); 29 | }; 30 | }); 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /lib/setup/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var Step = require('step'); 5 | var Hash = require('traverse/hash'); 6 | 7 | var mkdirP = require('./mkdir_p'); 8 | 9 | module.exports = function (cb) { 10 | var configDir = process.env.HOME + '/.config/stackvm'; 11 | var files = [ 'local', 'remote' ].reduce(function (acc, x) { 12 | acc[x] = configDir + '/' + x + '.json'; 13 | return acc; 14 | }, {}); 15 | 16 | Step( 17 | function () { mkdirP(configDir, 0700, this) }, 18 | function (err) { 19 | Hash(files).forEach((function (file) { 20 | path.exists(file, this.parallel().bind(this, null)); 21 | }).bind(this)); 22 | }, 23 | function (err) { 24 | if (err) { cb(err); return } 25 | 26 | this.parallel()(null); 27 | 28 | Hash(Object.keys(files), [].slice.call(arguments, 1)) 29 | .filter(function (x) { return !x }) 30 | .forEach((function (x, file) { 31 | var filename = configDir + '/' + file + '.json'; 32 | var ws = fs.createWriteStream(filename, { mode : 0600 }); 33 | ws.on('close', this.parallel().bind(this, null)); 34 | ws.write(JSON.stringify({})); 35 | ws.end(); 36 | }).bind(this)) 37 | ; 38 | }, 39 | function (err) { 40 | if (err) cb(err) 41 | else { 42 | Hash(files).forEach((function (file) { 43 | ConfigFile(file, this.parallel()); 44 | }).bind(this)); 45 | } 46 | }, 47 | function (err) { 48 | if (err) cb(err) 49 | else cb(null, Hash.zip( 50 | Object.keys(files), [].slice.call(arguments, 1) 51 | )); 52 | } 53 | ); 54 | }; 55 | 56 | exports.ConfigFile = ConfigFile; 57 | function ConfigFile (filename, cb) { 58 | var self = new EventEmitter; 59 | self.update = function (f) { 60 | fs.readFile(filename, function (err, data) { 61 | var hash = JSON.parse(data); 62 | var result = f(hash); 63 | var updated = result === undefined ? hash : result; 64 | var ws = fs.createWriteStream(filename, { mode : 0600 }); 65 | ws.on('close', self.emit.bind(self, 'update', updated)); 66 | ws.write(JSON.stringify(updated)); 67 | ws.end(); 68 | }); 69 | return self; 70 | }; 71 | 72 | fs.readFile(filename, function (err, data) { 73 | if (err) cb(err); 74 | else cb(null, Hash.merge( 75 | self, { data : JSON.parse(data) } 76 | )); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /lib/setup/deploy.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var sys = require('sys'); 3 | var Hash = require('traverse/hash'); 4 | var Step = require('step'); 5 | 6 | var Config = require('./config.js'); 7 | var mkdirP = require('./mkdir_p'); 8 | 9 | exports.deploy = function (opts) { 10 | var self = this; 11 | var dirs = [ 'users', 'data', 'logs' ]; 12 | var cb = opts.done; 13 | 14 | Step( 15 | function () { self.hasQemu(this) }, 16 | function (hasQemu) { 17 | if (!hasQemu && opts.qemu != false) { 18 | cb('Qemu not detected in $PATH with `which qemu`. ' 19 | + ' If you still want to install, specify --no-qemu'); 20 | } 21 | else { 22 | dirs.forEach((function (dir) { 23 | mkdirP(opts.base + '/' + dir, 0700, this.parallel()); 24 | }).bind(this)); 25 | } 26 | }, 27 | function (err) { 28 | if (err) cb(err) 29 | else Config(function (err, config) { 30 | if (err) cb(err) 31 | else config.local 32 | .update(function (data) { 33 | var p = opts.port || 9000; 34 | while (Hash(data).some(function (x) { 35 | return x.port == p 36 | })) p++; 37 | 38 | data[opts.name] = { 39 | directory : opts.base, 40 | port : p, 41 | pid : null, 42 | }; 43 | }) 44 | .on('update', cb.bind({}, null)) 45 | }) 46 | } 47 | ); 48 | }; 49 | 50 | function hasQemu (cb) { 51 | var which = spawn('which', ['qemu']); 52 | which.on('exit', function (code) { 53 | cb(code == 0); 54 | }); 55 | }; 56 | 57 | exports.undeploy = function (name, cb) { 58 | Config(function (err, config) { 59 | if (err) cb(err) 60 | else config.local 61 | .update(function (data) { delete data[name] }) 62 | .on('update', cb.bind({}, null)) 63 | }) 64 | }; 65 | -------------------------------------------------------------------------------- /lib/setup/mkdir_p.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | module.exports = function mkdirP (p, mode, f) { 5 | var cb = f || function () {}; 6 | if (p.charAt(0) != '/') { cb('Relative path: ' + p); return } 7 | 8 | var ps = path.normalize(p).split('/'); 9 | path.exists(p, function (exists) { 10 | if (exists) cb(null); 11 | else mkdirP(ps.slice(0,-1).join('/'), mode, function (err) { 12 | if (err && err.errno != process.EEXIST) cb(err) 13 | else fs.mkdir(p, mode, cb); 14 | }); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/setup/starter.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var spawn = require('child_process').spawn; 3 | var Step = require('step'); 4 | var Config = require('./config'); 5 | 6 | exports.start = function (name, cb) { 7 | Config(function (err, config) { 8 | if (err) { cb(err); return } 9 | 10 | var params = config.local.data[name]; 11 | if (!params) { cb('Unknown name: ' + name); return } 12 | var dir = params.directory; 13 | 14 | fs.open(dir + '/logs/' + Date.now(), 'w', function (err, logFd) { 15 | var server = spawn('node', 16 | [ __dirname + '/../../bin/server.js', params.port ], 17 | { cwd : dir, customFds : [ -1, logFd, logFd ] } 18 | ); 19 | config.local.update(function (data) { 20 | data[name].pid = server.pid; 21 | cb(null); 22 | }); 23 | }); 24 | }); 25 | }; 26 | 27 | exports.stop = function (name, cb) { 28 | Config(function (err, config) { 29 | if (err) { cb(err); return } 30 | var params = config.local.data[name]; 31 | if (!params) { cb('Unknown name: ' + name); return } 32 | console.log('pid = ' + params.pid); 33 | cb(null); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /lib/setup/user.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var Step = require('step'); 3 | 4 | exports.createUser = function (config, name, cb) { 5 | var root = config.directories.users + '/' + name; 6 | Step( 7 | function () { 8 | if (name in ['.','..',''] || name.match(/\//)) { 9 | cb('Names cannot contain "/" nor equal "." nor ".." nor ""'); 10 | } 11 | else { 12 | fs.mkdir(root, 0700 , this); 13 | } 14 | }, 15 | function (err) { 16 | if (err) cb(err); 17 | else { 18 | fs.mkdir(root + '/disks', 0700 , this.parallel()); 19 | fs.mkdir(root + '/thumbs', 0700 , this.parallel()); 20 | } 21 | }, 22 | function (err) { 23 | if (err) cb(err); 24 | else cb(null); 25 | } 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/web.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | var player = require('./web/player'); 5 | var User = require('./models/user'); 6 | var Traverse = require('traverse'); 7 | var Hash = require('traverse/hash'); 8 | 9 | var ejs = require('ejs'); 10 | var root = __dirname + '/..'; 11 | var views = Hash.map({ 12 | userbar : 'userbar.html' 13 | }, function (base) { 14 | var file = root + '/views/' + base; 15 | var body = fs.readFileSync(file, 'utf8'); 16 | fs.watchFile(file, function () { 17 | try { body = fs.readFileSync(file, 'utf8') } 18 | catch (_) { } 19 | }); 20 | return function (vars) { 21 | return ejs.render(body, { locals : vars }); 22 | }; 23 | }); 24 | 25 | module.exports = function (app, users) { 26 | function locals (req, vars) { 27 | var user = users[req.session.name]; 28 | return Hash.merge({ 29 | Hash : Hash, 30 | user : user, 31 | views : views, 32 | request : req, 33 | tabs : user 34 | ? [].concat(user.admin ? [ 35 | { path : '/admin', name : 'administer' } 36 | ] : []) 37 | : [] 38 | , 39 | }, vars); 40 | } 41 | 42 | app.configure(function () { 43 | app.set('views', root + '/views'); 44 | app.register('.html', require('ejs')); 45 | app.set('view engine', 'html'); 46 | }); 47 | 48 | app.get('/', function (req, res) { 49 | res.render('pages/index.html', { locals : locals(req, {}) }); 50 | }); 51 | 52 | app.post('/', function (req, res) { 53 | var user = users[req.body.user]; 54 | if (user && user.hash == User.hash(req.body.pass)) { 55 | req.session.regenerate(function (err) { 56 | if (err) throw err; 57 | req.session.name = user.name; 58 | res.render('pages/index.html', { locals : locals(req, {}) }); 59 | }); 60 | } 61 | else { 62 | res.send('failed'); 63 | } 64 | }); 65 | 66 | app.get('/ycdemo', function (req, res) { 67 | req.session.regenerate(function (err) { 68 | if (err) throw err; 69 | req.session.name = 'ycdemo'; 70 | res.redirect('/player/'); 71 | }); 72 | }); 73 | 74 | app.post('/logout', function (req, res) { 75 | req.session.regenerate(function(err) { 76 | if (err) throw err; 77 | res.render('pages/index.html', { locals : locals(req, {}) }); 78 | }); 79 | }); 80 | 81 | app.get(/^\/disks\/([^\/]+)\/([^\/]+)\/thumbnail/, function (req, res) { 82 | var disk = req.params[0]; 83 | var addr = req.params[1]; 84 | var defFile = root + '/static/img/default-vm.png'; 85 | 86 | if (!req.session.name || disk.match(/\.\./) || addr.match(/\.\./)) { 87 | res.sendfile(defFile); 88 | } 89 | else { 90 | var file = process.cwd() + '/users/' + req.session.name 91 | + '/thumbs/' + disk + '/' + addr + '.png'; 92 | path.exists(file, function (exists) { 93 | res.sendfile(exists ? file : defFile); 94 | }); 95 | } 96 | }); 97 | 98 | app.get('/admin', function (req, res) { 99 | res.render('pages/admin.html', { locals : locals(req, { 100 | users : users 101 | }) }); 102 | }); 103 | 104 | app.post('/admin/users/delete', function (req, res) { 105 | res.render('pages/admin.html', { locals : locals(req, { 106 | users : Hash.filter(users, function (u) { 107 | return u.name != req.body.name; 108 | }) 109 | }) }); 110 | console.log('TODO: actually delete users'); 111 | }); 112 | 113 | app.post('/admin/users/create', function (req, res) { 114 | var u = {}; 115 | u[req.body.name] = { name : req.body.name }; 116 | res.render('pages/admin.html', { locals : locals(req, { 117 | users : Hash.merge(users, u) 118 | }) }); 119 | console.log('TODO: actually create users'); 120 | }); 121 | 122 | player(app, root); 123 | }; 124 | -------------------------------------------------------------------------------- /lib/web/player.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var glob = require('glob'); 3 | var Hash = require('traverse/hash'); 4 | 5 | module.exports = function (app, root) { 6 | var player = { 7 | stylesheets : 8 | 'chat.css fb.css login.css sidebar.css window.css workspace.css' 9 | .split(' ') 10 | .map(function (x) { return '/css/player/' + x }) 11 | , 12 | scripts : 13 | [ 14 | 'vendor/jquery.js', 15 | 'vendor/jquery-ui.js', 16 | 'vendor/jquery.mousewheel.js', 17 | 'util/events.js', 18 | 'util/keymap.js', 19 | ].concat( 20 | glob.globSync(root + '/static/js/*.js'), 21 | glob.globSync(root + '/static/js/ui/*.js'), 22 | glob.globSync(root + '/static/js/ui/*/*.js') 23 | ).map(function (x) { 24 | return '/js/' + x.replace(root + '/static/js/', '') 25 | }) 26 | , 27 | }; 28 | 29 | var bundle = Hash.map(player, function (x) { 30 | return x.map(function (file) { 31 | return fs.readFileSync(root + '/static' + file); 32 | }).join('\n'); 33 | }); 34 | 35 | app.get('/css/player.css', function (req, res) { 36 | res.send(bundle.stylesheets, { 'Content-Type' : 'text/css' }); 37 | }); 38 | 39 | app.get('/js/player.js', function (req, res) { 40 | res.send(bundle.scripts, { 'Content-Type' : 'text/html' }); 41 | }); 42 | 43 | app.get('/player/', function (req, res) { 44 | app.configure('development', function () { 45 | res.render('player.html', { 46 | locals : player, 47 | layout : false, 48 | }); 49 | }); 50 | 51 | app.configure('production', function(){ 52 | res.render('player.html', { 53 | locals : { 54 | stylesheets : [ '/css/player.css' ], 55 | scripts : [ '/js/player.js' ], 56 | }, 57 | layout : false, 58 | }); 59 | }); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "stackvm", 3 | "version" : "0.0.5", 4 | "descrption" : "Control virtual machines in a browser", 5 | "repository" : { 6 | "type" : "git", 7 | "url" : "http://github.com/substack/stackvm.git" 8 | }, 9 | "dependencies" : { 10 | "dnode" : ">=0.2.13", 11 | "traverse" : ">=0.2.0", 12 | "glob" : ">=1.0.2", 13 | "express" : ">=0.6.1", 14 | "ejs" : ">=0.2.0", 15 | "rfb" : ">=0.0.5", 16 | "png" : ">=1.0.3", 17 | "base64" : ">=1.0.1", 18 | "video" : ">=1.0.2", 19 | "supermarket" : ">=1.0.1", 20 | "cart" : ">=1.0.6", 21 | "step" : ">=0.0.3" 22 | }, 23 | "engine" : [ "node >=0.2.0" ], 24 | "bin" : { "stackvm" : "./bin/cli.js" } 25 | } 26 | -------------------------------------------------------------------------------- /static/css/player/chat.css: -------------------------------------------------------------------------------- 1 | .chat-window { 2 | position: fixed; 3 | right: 10px; 4 | bottom: 10px; 5 | width: 300px; 6 | height: 300px; 7 | background-color: white; 8 | color: black; 9 | z-index: 100; 10 | cursor: auto; 11 | } 12 | 13 | .chat-window form input { 14 | margin-left: 2px; 15 | width: 288px; 16 | } 17 | 18 | .chat-title { 19 | background-color: rgb(60,60,60); 20 | color: white; 21 | font-weight: bold; 22 | padding: 0.3em; 23 | } 24 | 25 | .chat-x { 26 | position: absolute; 27 | top: 0px; 28 | right: 0px; 29 | color: rgb(255,150,100); 30 | width: 1.6em; 31 | height: 1.4em; 32 | } 33 | 34 | .chat-x:hover { 35 | background-color: white; 36 | cursor: pointer; 37 | } 38 | 39 | .chat-body { 40 | width: 285px; 41 | height: 230px; 42 | padding: 0.5em; 43 | overflow-y: scroll; 44 | } 45 | 46 | .chat-body p { 47 | margin-top: 0em; 48 | margin-bottom: 0em; 49 | } 50 | 51 | span.chat-who { 52 | font-weight: bold; 53 | margin-right: 1em; 54 | } 55 | 56 | span.chat-msg { } 57 | 58 | .chat-share { } 59 | 60 | .chat-share span { 61 | margin-left: 1em; 62 | } 63 | 64 | .chat-share a { 65 | margin-left: 1em; 66 | color: rgb(255,150,100); 67 | cursor : pointer; 68 | } 69 | 70 | .chat-resource a { 71 | color: rgb(255,150,100); 72 | cursor: pointer; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /static/css/player/fb.css: -------------------------------------------------------------------------------- 1 | .fb { /* framebuffer container */ 2 | background-color: black; 3 | cursor: url(/img/empty-cursor.png), auto; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /static/css/player/login.css: -------------------------------------------------------------------------------- 1 | div#backdrop { 2 | background-color: rgb(20,20,20); 3 | position: fixed; 4 | top: 0px; 5 | bottom: 0px; 6 | left: 0px; 7 | right : 0px; 8 | overflow: hidden; 9 | } 10 | 11 | form#login { 12 | border-color: white; 13 | border-style: solid; 14 | border-width: 5px; 15 | background-color: rgb(100,100,100); 16 | padding: 15px; 17 | width: 417px; 18 | margin: auto; 19 | margin-top: 5%; 20 | text-align: center; 21 | } 22 | 23 | form#login img { 24 | margin-bottom: 1em; 25 | } 26 | 27 | form#login input { 28 | width: 10em; 29 | padding: 0.5em; 30 | border-color: black; 31 | border-style: solid; 32 | border-width: 2px; 33 | } 34 | 35 | form#login input[type='submit'] { 36 | width: 11.2em; 37 | } 38 | 39 | div.sign-in-failed { 40 | border-color: black; 41 | border-style: solid; 42 | border-width: 2px; 43 | background-color: rgb(255,71,71); 44 | color: white; 45 | font-weight: bold; 46 | width: 8.75em; 47 | padding: 0.5em; 48 | margin: auto; 49 | } 50 | 51 | -------------------------------------------------------------------------------- /static/css/player/sidebar.css: -------------------------------------------------------------------------------- 1 | img#logo { 2 | cursor: pointer; 3 | } 4 | 5 | div.sidebar { 6 | position: fixed; 7 | width: 200px; 8 | top: 5px; 9 | bottom: 100px; 10 | left: 10px; 11 | color: white; 12 | overflow-x: hidden; 13 | overflow-y: auto; 14 | cursor: auto; 15 | z-index: 200; 16 | } 17 | 18 | div.sidebar-menu { 19 | background-color: rgb(100,100,100); 20 | position: fixed; 21 | width: 200px; 22 | top: 55px; 23 | bottom: 100px; 24 | left: 10px; 25 | color: white; 26 | overflow-x: hidden; 27 | overflow-y: auto; 28 | } 29 | 30 | div.side-menu { } 31 | 32 | div.side-menu-body { 33 | padding-left: 0.5em; 34 | padding-right: 0.5em; 35 | } 36 | 37 | div.side-menu-title { 38 | background-color: rgb(60,60,60); 39 | font-weight: bold; 40 | padding: 8px; 41 | border-bottom-color: rgb(20,20,20); 42 | border-bottom-style: solid; 43 | border-bottom-width: 4px; 44 | cursor: pointer; 45 | } 46 | 47 | div.side-menu-title:hover { 48 | background-color: rgb(140,140,140); 49 | } 50 | 51 | div.sidebar a { 52 | text-decoration: underline; 53 | font-weight: bold; 54 | cursor: pointer; 55 | } 56 | 57 | div.sidebar div.back { 58 | border-color: white; 59 | border-style: solid; 60 | border-width: 2px; 61 | background-color: rgb(100,100,100); 62 | color: white; 63 | height: 3em; 64 | text-align: center; 65 | cursor: pointer; 66 | display: table-cell; 67 | vertical-align: middle; 68 | width: 200px; 69 | } 70 | 71 | div.sidebar div.back:hover { 72 | background-color: rgb(200,200,200); 73 | color: rgb(50,50,50); 74 | } 75 | 76 | .contact { font-weight: bold; } 77 | 78 | .contact-online { 79 | cursor: pointer; 80 | font-weight: bold; 81 | color: rgb(200,255,150); 82 | } 83 | 84 | .contact-offline { 85 | color: rgb(200,200,200); 86 | } 87 | 88 | .disk { 89 | font-weight: bold; 90 | margin-bottom: 1em; 91 | } 92 | 93 | .disk-filename { 94 | font-weight: normal; 95 | font-size: 0.85em; 96 | color: rgb(200,200,200); 97 | } 98 | 99 | .disk-spawn { 100 | font-size: 0.85em; 101 | color: rgb(250,220,130); 102 | font-weight: normal; 103 | } 104 | 105 | .disk-spawn span a { 106 | font-size: 0.9em; 107 | font-weight: bold; 108 | text-decoration: none; 109 | color: rgb(255,150,100); 110 | cursor: pointer; 111 | } 112 | 113 | .disk-spawn span a:hover { 114 | text-decoration: underline; 115 | } 116 | 117 | ol.instances li { 118 | cursor: pointer; 119 | } 120 | 121 | -------------------------------------------------------------------------------- /static/css/player/window.css: -------------------------------------------------------------------------------- 1 | div.vm-window { 2 | position: fixed; 3 | left: 220; 4 | top: 20; 5 | background-color: rgb(100,100,100); 6 | padding: 1px; 7 | border-width: 2px; 8 | border-style: solid; 9 | border-color: rgb(150,150,150); 10 | z-index: 100; 11 | } 12 | 13 | div.title-bar { 14 | position: float; 15 | margin-top: -32px; 16 | margin-left: -3px; 17 | margin-bottom: 0px; 18 | height: 30px; 19 | background-color: white; 20 | color: black; 21 | padding-left: 0.5em; 22 | } 23 | 24 | div.title-bar div.title-text { 25 | display: table-cell; 26 | vertical-align: middle; 27 | height: 30px; 28 | cursor: pointer; 29 | } 30 | 31 | .title-text-drag { 32 | background-color: white; 33 | color: black; 34 | width: 10em; 35 | z-index: 1000000; 36 | } 37 | 38 | div.title-bar img.window-button { 39 | float: right; 40 | margin-right: 10px; 41 | margin-top: 5px; 42 | width: 20px; 43 | height: 20px; 44 | cursor: pointer; 45 | } 46 | 47 | div.title-bar img.menu-button { 48 | float: left; 49 | } 50 | 51 | div.window-menu { 52 | position: absolute; 53 | display: block; 54 | top: 0px; 55 | left: 0px; 56 | } 57 | 58 | div.window-menu div.menu-item { 59 | display: block; 60 | height: 1em; 61 | width: 10em; 62 | font-weight: bold; 63 | padding: 4px; 64 | background-color: white; 65 | color: black; 66 | border-width: 2px; 67 | border-style: solid; 68 | border-color: black; 69 | } 70 | 71 | div.window-menu div.menu-item:hover { 72 | background-color: rgb(150,30,30); 73 | color: white; 74 | border-color: white; 75 | cursor: pointer; 76 | } 77 | -------------------------------------------------------------------------------- /static/css/player/workspace.css: -------------------------------------------------------------------------------- 1 | div#workspace { 2 | position: fixed; 3 | top: 0px; 4 | bottom: 0px; 5 | left: 0px; 6 | right : 0px; 7 | overflow: hidden; 8 | cursor: move; 9 | } 10 | 11 | div#quick-bar { 12 | position: fixed; 13 | bottom: 0px; 14 | left: 0px; 15 | right: 0px; 16 | height: 100px; 17 | z-index: 20; 18 | cursor: auto; 19 | } 20 | 21 | div#quick-bar div { 22 | float: left; 23 | width: 120px; 24 | height: 95px; 25 | color: white; 26 | text-align: center; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /static/css/site/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(70,70,70); 3 | } 4 | 5 | #logo { 6 | position: relative; 7 | margin: auto; 8 | width: 417px; 9 | z-index: 2; 10 | } 11 | 12 | #block { 13 | border-color: rgb(50,50,50); 14 | border-style: solid; 15 | border-width: 2px; 16 | margin: auto; 17 | width: 880px; 18 | margin-bottom: 10px; 19 | margin-top: -10px; 20 | } 21 | 22 | #userbar { 23 | position: relative; 24 | width: 800px; 25 | height: 2em; 26 | margin: auto; 27 | background-color: rgb(230,70,70); 28 | padding: 0.5em; 29 | padding-left: 2em; 30 | padding-right: 2em; 31 | color: rgb(230,230,230); 32 | 33 | border-color: rgb(150,100,100); 34 | border-style: solid; 35 | border-width: 10px; 36 | border-bottom-width: 0px; 37 | 38 | z-index: 1; 39 | 40 | font-weight: bold; 41 | } 42 | 43 | #tabs { 44 | position: absolute; 45 | right: 5px; 46 | top: 0.9em; 47 | } 48 | 49 | #tabs .tab { 50 | padding: 0.5em; 51 | height: 1em; 52 | } 53 | 54 | #tabs .tab a { 55 | text-decoration: none; 56 | } 57 | 58 | #tabs .active { 59 | background-color: rgb(250,240,240); 60 | } 61 | 62 | #tabs .active a { 63 | color: rgb(200,0,0); 64 | } 65 | 66 | #tabs .inactive { 67 | background-color: rgb(245,100,100); 68 | } 69 | 70 | #tabs .inactive a { 71 | color: rgb(250,240,240); 72 | } 73 | 74 | #content { 75 | margin: auto; 76 | width: 800px; 77 | background-color: rgb(220,220,220); 78 | padding: 2em; 79 | border-color: rgb(100,100,100); 80 | border-style: solid; 81 | border-width: 10px; 82 | border-top-width: 0px; 83 | } 84 | 85 | table#admin-users tr th { 86 | width: 300px; 87 | background-color: rgb(130,200,130); 88 | color: rgb(255,240,240); 89 | text-align: left; 90 | padding: 0.5em; 91 | } 92 | 93 | table#admin-users tr td { 94 | border-bottom-style: solid; 95 | border-bottom-width: 1px; 96 | border-bottom-color: rgb(100,100,100); 97 | } 98 | 99 | table#admin-users tr td.buttons { 100 | width: 50px; 101 | } 102 | -------------------------------------------------------------------------------- /static/img/buttons/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/buttons/close.png -------------------------------------------------------------------------------- /static/img/buttons/fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/buttons/fullscreen.png -------------------------------------------------------------------------------- /static/img/buttons/menu-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/buttons/menu-down.png -------------------------------------------------------------------------------- /static/img/buttons/menu-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/buttons/menu-up.png -------------------------------------------------------------------------------- /static/img/buttons/minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/buttons/minimize.png -------------------------------------------------------------------------------- /static/img/default-vm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/default-vm.png -------------------------------------------------------------------------------- /static/img/empty-cursor.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/empty-cursor.cur -------------------------------------------------------------------------------- /static/img/empty-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/empty-cursor.png -------------------------------------------------------------------------------- /static/img/icons/tux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/icons/tux.png -------------------------------------------------------------------------------- /static/img/stackvm-200x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/stackvm-200x48.png -------------------------------------------------------------------------------- /static/img/stackvm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkrumins/stackvm/ac76822020c0d0085ed3a4dc3cd2fb5756bb0d02/static/img/stackvm.png -------------------------------------------------------------------------------- /static/js/session.js: -------------------------------------------------------------------------------- 1 | function Session (cb) { 2 | DNode().connect( 3 | { ping : 3600*1000, timeout : 100 }, 4 | function (remote, conn) { 5 | remote.session(function (err, account) { 6 | if (err) cb(err); 7 | else cb(null, new UI(account)); 8 | }); 9 | 10 | function reconnect () { 11 | if (window.console) console.log('reconnecting'); 12 | conn.reconnect(3000, function f (err) { 13 | if (err) { 14 | if (window.console) console.log(err); 15 | reconnect(); 16 | } 17 | else { 18 | if (window.console) console.log('reconnected'); 19 | } 20 | }); 21 | } 22 | 23 | conn.on('timeout', reconnect); 24 | conn.on('end', reconnect); 25 | } 26 | ); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /static/js/site.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | DNode.connect(function (remote) { 3 | remote.session(function (err, account) { 4 | if (!err) Hash(account.disks).forEach(registerDisk); 5 | }); 6 | }); 7 | }); 8 | 9 | function registerDisk (disk, filename) { 10 | var diskElem = $('
') 11 | .addClass('proc') 12 | .text(filename) 13 | .appendTo($('#disks')) 14 | ; 15 | 16 | function registerProc (proc) { 17 | function makeImg () { 18 | var uri = '/disks/' + filename + '/' + proc.address + '/thumbnail' 19 | + '?' + Math.floor(Math.random() * 1e12); 20 | return $('').attr('src', uri).width(200).height(150); 21 | } 22 | 23 | var procElem = $('
') 24 | .addClass('proc') 25 | .append(makeImg) 26 | .appendTo(diskElem) 27 | ; 28 | proc.subscribe(function (sub) { 29 | sub.on('exit', function () { 30 | procElem.remove(); 31 | }); 32 | 33 | sub.on('thumb', function () { 34 | procElem.find('img').remove(); 35 | procElem.append(makeImg()); 36 | }); 37 | }); 38 | } 39 | 40 | Hash(disk.processes).forEach(registerProc); 41 | 42 | disk.subscribe(function (sub) { 43 | sub.on('spawn', registerProc); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /static/js/ui.js: -------------------------------------------------------------------------------- 1 | function UI (account) { 2 | if (!(this instanceof UI)) return UI(account); 3 | var contacts = account.contacts; 4 | var disks = account.disks; 5 | 6 | var workspace = new Workspace; 7 | var sidebar = new SideBar({ engines : ['qemu','vmware'] }); 8 | var taskbar = new TaskBar; 9 | 10 | workspace.element.append( 11 | sidebar.element, 12 | taskbar.element 13 | ); 14 | 15 | // inter-dependent ui hooks: 16 | workspace.on('minimize', function (win) { 17 | taskbar.push(win); 18 | }); 19 | 20 | sidebar.on('attach', function (proc) { 21 | var win = new Window({ remoteFB : proc.fb, proc : proc }); 22 | workspace.attachWindow(win); 23 | }); 24 | 25 | workspace.on('attach', function (proc) { 26 | var win = new Window({ remoteFB : proc.fb, proc : proc }); 27 | workspace.attachWindow(win); 28 | }); 29 | 30 | sidebar.on('chat', function (contact) { 31 | if (workspace.hasChat(contact.name)) return; 32 | var chat = new ChatWindow(account.name, contact); 33 | workspace.addChat(chat); 34 | }); 35 | 36 | taskbar.on('pop', function (win) { 37 | workspace.attachWindow(win); 38 | }); 39 | 40 | Hash(disks).forEach(sidebar.addDisk); 41 | Hash(contacts).forEach(function (contact) { 42 | contact.subscribe(function (sub) { 43 | sub.on('message', function (msg) { 44 | if (!workspace.hasChat(contact.name)) { 45 | var chat = new ChatWindow(account.name, contact); 46 | workspace.addChat(chat); 47 | } 48 | workspace.routeChat(contact, msg); 49 | }); 50 | sub.on('share', function (type, res) { 51 | if (!workspace.hasChat(contact.name)) { 52 | var chat = new ChatWindow(account.name, contact); 53 | workspace.addChat(chat); 54 | } 55 | workspace.routeResource(contact, type, res); 56 | }); 57 | 58 | }); 59 | sidebar.addContact(contact); 60 | }); 61 | 62 | this.element = workspace.element; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /static/js/ui/chat.js: -------------------------------------------------------------------------------- 1 | ChatWindow.prototype = new EventEmitter; 2 | function ChatWindow (me, contact) { 3 | if (!(this instanceof ChatWindow)) return new ChatWindow(); 4 | var self = this; 5 | self.contact = contact; 6 | 7 | var body = $('
') 8 | .addClass('chat-body') 9 | .droppable({ 10 | //accept : '.title-text-drag', 11 | drop : function (ev, ui) { 12 | var proc = $(ui.draggable).data('proc'); 13 | body.append($('
') 14 | .addClass('chat-share') 15 | .append( 16 | $('').text(proc.name), 17 | $('').text('Share: '), 18 | $('') 19 | .text('[r]') 20 | .click(function () { 21 | $(this).parent() 22 | .empty() 23 | .text('Sharing ' + proc.name + ' (r)') 24 | ; 25 | var rule = {}; 26 | rule[contact.name] = { input : false, view : true }; 27 | contact.share('process', proc.address, rule); 28 | }) 29 | , 30 | $('') 31 | .text('[rw]') 32 | .click(function () { 33 | $(this).parent() 34 | .empty() 35 | .text('Sharing ' + proc.name + ' (rw)') 36 | ; 37 | var rule = {}; 38 | rule[contact.name] = { input : true, view : true }; 39 | contact.share('process', proc.address, rule); 40 | }) 41 | ) 42 | ); 43 | } 44 | }) 45 | ; 46 | 47 | self.element = $('
') 48 | .addClass('chat-window') 49 | .append( 50 | $('
') 51 | .addClass('chat-title') 52 | .text(contact.name) 53 | .append($('
') 54 | .addClass('chat-x') 55 | .text('[x]') 56 | .click(function () { 57 | self.emit('close'); 58 | }) 59 | ) 60 | , 61 | body, 62 | $('
') 63 | .append( 64 | $('') 65 | .attr('name', 'msg') 66 | ) 67 | .submit(function (ev) { 68 | ev.preventDefault(); 69 | var msg = $(this.elements.msg).val(); 70 | self.addMessage(me, msg); 71 | contact.message(msg); 72 | $(this.elements.msg).val(''); 73 | }) 74 | ) 75 | ; 76 | 77 | self.addMessage = function (who, msg) { 78 | body.append($('

').append( 79 | $('').addClass('chat-who').text(who), 80 | $('').addClass('chat-msg').text(msg) 81 | )); 82 | $('.chat-body').animate({ 83 | scrollTop : $('.chat-body').attr('scrollHeight') 84 | }, 500); 85 | }; 86 | 87 | self.say = function (msg) { 88 | self.addMessage(me, msg); 89 | contact.message(msg); 90 | }; 91 | 92 | self.addResource = function (contact, type, res) { 93 | var elem = { 94 | process : function () { 95 | return $('

').append( 96 | contact.name + ' shares ', 97 | $('') 98 | .text(res.filename) 99 | .click(function () { 100 | self.emit('attach', res); 101 | }) 102 | , 103 | ' [' + res.mode + ']' 104 | ); 105 | }, 106 | disk : function () { 107 | return $('

').text('Sharing disks not yet implemented'); 108 | } 109 | }[type](); 110 | body.append(elem.addClass('chat-resource')); 111 | }; 112 | } 113 | 114 | -------------------------------------------------------------------------------- /static/js/ui/fb.js: -------------------------------------------------------------------------------- 1 | // A no-frills graphical container for the remote VM framebuffer 2 | // Renders the framebuffer and handles the keyboard and mouse 3 | 4 | FB.prototype = new EventEmitter; 5 | function FB (remote) { 6 | var self = this; 7 | 8 | var input = remote.input; 9 | var encoder = remote.encoder; 10 | var size = remote.size; 11 | 12 | var mouseCoords = null; 13 | 14 | var focus = false; 15 | self.focus = function () { 16 | focus = true; 17 | self.element.focus(); 18 | }; 19 | 20 | self.unfocus = function () { 21 | focus = false; 22 | $(window).focus(); 23 | }; 24 | 25 | var mouseMask = 0; 26 | 27 | self.element = $('

') 28 | .addClass('fb') 29 | .attr('tabindex', 0) // so the div can receive focus 30 | .width(size.width) 31 | .height(size.height) 32 | .mousemove(function (ev) { 33 | self.focus(); 34 | if (focus && input) { 35 | var pos = calcMousePos(ev); 36 | input.sendPointer(pos.x, pos.y, mouseMask); 37 | } 38 | }) 39 | .mousedown(function (ev) { 40 | if (focus) mouseMask = 1; 41 | var pos = calcMousePos(ev); 42 | if (input) input.sendPointer(pos.x, pos.y, mouseMask); 43 | ev.preventDefault(); 44 | }) 45 | .mouseup(function (ev) { 46 | if (focus) mouseMask = 0; 47 | var pos = calcMousePos(ev); 48 | if (input) input.sendPointer(pos.x, pos.y, mouseMask); 49 | ev.preventDefault(); 50 | }) 51 | .mousewheel(function (ev, delta) { 52 | var pos = calcMousePos(ev); 53 | if (delta > 0 && input) { // mouse up 54 | input.sendPointer(pos.x, pos.y, 1 << 3); 55 | input.sendPointer(pos.x, pos.y, 0); 56 | } 57 | else if (input) { 58 | input.sendPointer(pos.x, pos.y, 1 << 4); 59 | input.sendPointer(pos.x, pos.y, 0); 60 | } 61 | ev.preventDefault(); 62 | }) 63 | // Other events should just call this element's key events when key 64 | // events occur elsewhere but this vm has focus 65 | .keydown(function (ev) { 66 | if (focus && input) { 67 | input.sendKeyDown(KeyMapper.getKeySym(ev.keyCode)); 68 | ev.preventDefault(); 69 | } 70 | }) 71 | .keyup(function (ev) { 72 | if (focus && input) { 73 | input.sendKeyUp(KeyMapper.getKeySym(ev.keyCode)); 74 | ev.preventDefault(); 75 | } 76 | }) 77 | ; 78 | 79 | function calcMousePos (ev) { 80 | var x = ev.pageX - self.element.offset().left; 81 | var y = ev.pageY - self.element.offset().top; 82 | return { x : x, y : y }; 83 | } 84 | 85 | var display = new Display; 86 | display.resize(size); 87 | self.element.append(display.element); 88 | 89 | encoder.subscribe(function (sub) { 90 | sub.on('end', function () { self.emit('end') }); 91 | 92 | sub.on('desktopSize', function (dims) { 93 | size = dims; 94 | self.emit('resize', dims); 95 | self.element 96 | .width(dims.width) 97 | .height(dims.height) 98 | ; 99 | display.resize(dims); 100 | }); 101 | 102 | sub.on('screenUpdate', function (update) { 103 | display.rawRect(update); 104 | }); 105 | 106 | sub.on('copyRect', function (rect) { 107 | display.copyRect(rect); 108 | }); 109 | }); 110 | 111 | encoder.requestRedraw(); 112 | } 113 | 114 | -------------------------------------------------------------------------------- /static/js/ui/fb/display.js: -------------------------------------------------------------------------------- 1 | function toImg (img64, imgType) { 2 | var imgTypes = { 3 | png : 'data:image/png;base64,', 4 | jpeg : 'data:image/jpeg;base64,', 5 | gif : 'data:image/gif;base64,' 6 | }; 7 | 8 | if (!imgTypes[imgType]) 9 | throw "Unknown imgType '" + imgType + "' was passed to toImg"; 10 | 11 | // TODO: use MHTML for IE6 12 | return $('').attr('src', imgTypes[imgType] + img64); 13 | } 14 | 15 | function CanvasDisplay () { 16 | var self = this; 17 | 18 | var canvas = $(''); 19 | self.element = $('
').addClass('canvasConsole'); 20 | self.element.append(canvas); 21 | 22 | var canvasHTML = canvas[0]; 23 | var context = canvasHTML.getContext('2d'); 24 | 25 | self.resize = function (dims) { 26 | canvas.attr('width', dims.width); 27 | canvas.attr('height', dims.height); 28 | self.element.width(dims.width); 29 | self.element.height(dims.height); 30 | }; 31 | 32 | self.rawRect = function (rect) { 33 | var img = toImg(rect.base64, rect.type); 34 | img.load(function () { 35 | context.drawImage(img[0], rect.x, rect.y, rect.width, rect.height); 36 | }); 37 | }; 38 | 39 | self.copyRect = function (rect) { 40 | context.drawImage( 41 | canvasHTML, 42 | rect.srcX, rect.srcY, rect.width, rect.height, 43 | rect.x, rect.y, rect.width, rect.height 44 | ); 45 | }; 46 | 47 | self.can = !!context; 48 | } 49 | 50 | function StackedDisplay () { 51 | var self = this; 52 | 53 | self.element = $('
').addClass('stackedConsole'); 54 | 55 | self.rawRect = function (rect) { 56 | var img = toImg(rect.base64, rect.type); 57 | img.css({ 58 | position : 'absolute', 59 | left : rect.x, 60 | top : rect.y, 61 | width : rect.width + 'px', 62 | height : rect.height + 'px', 63 | }); 64 | self.element.append(img); 65 | //if (fullScreen) cleanupImages(img); 66 | } 67 | 68 | self.resize = function (dims) { 69 | self.element.width(dims.width); 70 | self.element.height(dims.height); 71 | }; 72 | 73 | self.copyRect = function (rect) { 74 | console.log('got copyrect for stacked display'); 75 | } 76 | 77 | function cleanupImages (except) { 78 | $('img', self.element) 79 | .not(except) 80 | .remove() 81 | ; 82 | } 83 | 84 | self.can = true; 85 | } 86 | 87 | function Display () { 88 | var display = new CanvasDisplay; 89 | if (!display.can) 90 | display = new StackedDisplay; 91 | 92 | this.element = display.element; 93 | this.rawRect = display.rawRect; 94 | this.resize = display.resize; 95 | this.copyRect = display.copyRect; 96 | }; 97 | 98 | -------------------------------------------------------------------------------- /static/js/ui/sidebar.js: -------------------------------------------------------------------------------- 1 | SideBar.prototype = new EventEmitter; 2 | function SideBar (params) { 3 | var self = this; 4 | var engines = params.engines; 5 | 6 | var elements = { 7 | disks : $('
'), 8 | contacts : $('
'), 9 | screenshots : $('
'), 10 | screencasts : $('
'), 11 | }; 12 | 13 | var menu = new MenuStack; 14 | menu.push('main menu', $('
').append( 15 | $('

').append( 16 | $('').text('contacts') 17 | .click(function () { 18 | menu.push('contacts', elements.contacts); 19 | }) 20 | ), 21 | $('

').append( 22 | $('').text('disk images') 23 | .click(function () { 24 | menu.push('disk images', elements.disks); 25 | }) 26 | ) 27 | /* 28 | $('

').append( 29 | $('').text('screenshots') 30 | .click(function () { 31 | menu.push('screenshots', elements.screenshots) 32 | }) 33 | ), 34 | $('

').append( 35 | $('').text('screencasts') 36 | .click(function () { 37 | menu.push('screencasts', elements.screencasts) 38 | }) 39 | ) 40 | */ 41 | )); 42 | 43 | self.element = $('

').addClass('sidebar').append( 44 | $('') 45 | .attr('id', 'logo') 46 | .width(200).height(48) 47 | .attr('src', '/img/stackvm-200x48.png') 48 | .toggle( 49 | function () { $('.sidebar-menu').fadeOut(400) }, 50 | function () { $('.sidebar-menu').fadeIn(400) } 51 | ) 52 | , 53 | $('
').addClass('sidebar-menu').append(menu.element) 54 | ); 55 | 56 | self.addContact = function (contact) { 57 | var elem = $('
') 58 | .addClass('contact') 59 | .addClass('contact-' + (contact.online ? 'online' : 'offline')) 60 | .text(contact.name) 61 | .click(function () { 62 | if ($(this).hasClass('contact-online')) { 63 | self.emit('chat', contact); 64 | } 65 | }) 66 | .appendTo(elements.contacts) 67 | ; 68 | contact.subscribe(function (sub) { 69 | sub.on('online', function () { 70 | elem.removeClass('contact-offline').addClass('contact-online'); 71 | }); 72 | sub.on('offline', function () { 73 | elem.removeClass('contact-online').addClass('contact-offline'); 74 | }); 75 | }); 76 | }; 77 | 78 | self.addDisk = function (disk) { 79 | var engineLinks = $(''); 80 | engines.forEach(function (engine) { 81 | if (engine == 'vmware') return; 82 | engineLinks.append($('') 83 | .text(engine) 84 | .click(function () { disk.spawn(engine) }) 85 | , $('').text(' ')); 86 | }); 87 | 88 | var ol = $('
    ').addClass('instances').attr('start',0); 89 | 90 | var div = $('
    ') 91 | .addClass('disk') 92 | .text(disk.name) 93 | .append( 94 | $('
    ') 95 | .addClass('disk-filename') 96 | .text(disk.filename) 97 | , 98 | $('
    ') 99 | .addClass('disk-spawn') 100 | .text('Spawn in ') 101 | .append(engineLinks) 102 | , 103 | ol 104 | ) 105 | ; 106 | elements.disks.append(div); 107 | 108 | function addInstance (proc) { 109 | var elem = $('
  1. ') 110 | .text(proc.engine + ':' + proc.pid) 111 | .click(function () { self.emit('attach', proc) }) 112 | .appendTo(ol) 113 | ; 114 | proc.subscribe(function (sub) { 115 | sub.on('exit', function () { 116 | elem.remove(); 117 | }); 118 | }); 119 | }; 120 | 121 | function addScreenX(elem, url) { 122 | $('

    ').append($('') 123 | .attr('href', url) 124 | .text(url.split('/').slice(-1)[0]) 125 | ).appendTo(elem); 126 | } 127 | 128 | disk.subscribe(function (sub) { 129 | sub.on('spawn', addInstance); 130 | sub.on('screenshot', function (url) { 131 | addScreenX(elements.screenshots, url); 132 | }); 133 | sub.on('screencast', function (url) { 134 | addScreenX(elements.screencasts, url); 135 | }); 136 | }); 137 | Hash(disk.processes).forEach(addInstance); 138 | }; 139 | } 140 | 141 | -------------------------------------------------------------------------------- /static/js/ui/sidebar/menustack.js: -------------------------------------------------------------------------------- 1 | function MenuStack () { 2 | var self = this; 3 | 4 | var menuStack = []; 5 | self.element = $('

    ').attr('id','side-menu'); 6 | 7 | self.push = function (title, item) { 8 | if (menuStack.length) { 9 | self.top().children('.side-menu-body').hide(); 10 | } 11 | 12 | var index = menuStack.length; 13 | var elem = $('
    ') 14 | .addClass('side-menu') 15 | .height($(window).height - 55 - 100) 16 | .append( 17 | $('
    ') 18 | .addClass('side-menu-title') 19 | .text(title) 20 | .click(function () { 21 | while (menuStack.length - 1 > index) self.pop(); 22 | }) 23 | , 24 | $('
    ').addClass('side-menu-body').append(item) 25 | ) 26 | ; 27 | 28 | menuStack.push(elem); 29 | self.element.append(elem); 30 | }; 31 | 32 | self.pop = function () { 33 | menuStack.pop().remove(); 34 | if (menuStack.length) { 35 | self.top().children('.side-menu-body').show(); 36 | } 37 | }; 38 | 39 | self.top = function () { 40 | return menuStack.slice(-1)[0]; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /static/js/ui/taskbar.js: -------------------------------------------------------------------------------- 1 | TaskBar.prototype = new EventEmitter; 2 | function TaskBar (params) { 3 | var self = this; 4 | var vms = []; 5 | 6 | self.element = $('
    ') 7 | .attr('id','quick-bar') 8 | ; 9 | 10 | self.push = function (vm, host) { 11 | vms.push(vm); 12 | var div = $('
    ') 13 | .append( 14 | $('') 15 | .attr('src', '/img/icons/tux.png') 16 | .attr('width', 64) 17 | .attr('height', 75) 18 | , 19 | $('
    ').text(vm.name) 20 | ) 21 | .click(function () { 22 | self.emit('restore', vm, host); 23 | div.fadeOut(400, function () { div.remove() }); 24 | }) 25 | ; 26 | self.element.append(div); 27 | }; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /static/js/ui/window.js: -------------------------------------------------------------------------------- 1 | Window.prototype = new EventEmitter; 2 | function Window (params) { 3 | var self = this; 4 | var remoteFB = params.remoteFB; 5 | var proc = params.proc; 6 | self.proc = proc; 7 | self.addr = proc.addr; 8 | 9 | var fb = new FB(remoteFB); 10 | 11 | self.titleBar = new TitleBar(proc); 12 | 13 | if (!proc.shared) { 14 | self.titleBar.on('kill', function () { proc.kill() }); 15 | } 16 | 17 | fb.on('end', function () { self.emit('exit') }); 18 | 19 | self.element = $('
    ') 20 | .append(self.titleBar.element) 21 | .append(fb.element) 22 | .addClass('vm-window') 23 | .width(remoteFB.size.width) 24 | .height(remoteFB.size.height) 25 | .css('margin-bottom', -remoteFB.size.height - 6) 26 | .offset({ 27 | left : 220 + ($(window).width() - 220 - remoteFB.size.width) / 2, 28 | top : Math.max(40, ($(window).height() - remoteFB.size.height) / 2) 29 | }) 30 | .click(function (ev) { fb.focus() }) 31 | .draggable({ 32 | handle : self.titleBar.element, 33 | appendTo : 'body', 34 | stack : '.vm-window' 35 | }); 36 | ; 37 | 38 | fb.on('resize', function (dims) { 39 | self.element 40 | .width(dims.width) 41 | .height(dims.height) 42 | .css('margin-bottom', -dims.height - 6) 43 | ; 44 | self.titleBar.element.width(dims.width - 1); 45 | }); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /static/js/ui/window/titlebar.js: -------------------------------------------------------------------------------- 1 | TitleBar.prototype = new EventEmitter; 2 | function TitleBar (proc) { 3 | var self = this; 4 | 5 | var menu = $('
    ') 6 | .addClass('window-menu') 7 | .hide() 8 | .append( 9 | $('
    ') 10 | .addClass('menu-item') 11 | .text('restart') 12 | .click(function () { 13 | $('.menu-button').click(); 14 | self.emit('restart'); 15 | }) 16 | , 17 | $('
    ') 18 | .addClass('menu-item') 19 | .text('kill') 20 | .click(function () { 21 | $('.menu-button').click(); 22 | self.emit('kill'); 23 | }) 24 | , 25 | $('
    ') 26 | .addClass('menu-item') 27 | .text('screenshot') 28 | .click(function () { 29 | $('.menu-button').click(); 30 | self.emit('screenshot'); 31 | }) 32 | , 33 | $('
    ') 34 | .addClass('menu-item') 35 | .text('start screencast') 36 | .click(function () { 37 | if (/start/.test($(this).text())) { 38 | $(this).text('stop screencast'); 39 | self.emit('startScreencast'); 40 | } 41 | else { 42 | $(this).text('start screencast'); 43 | self.emit('stopScreencast'); 44 | } 45 | }) 46 | ) 47 | ; 48 | 49 | self.element = $('
    ') 50 | .addClass('title-bar') 51 | .append( 52 | $('') 53 | .attr('src','/img/buttons/close.png') 54 | .addClass('window-button') 55 | .click(function () { self.emit('close') }) 56 | , 57 | $('') 58 | .attr('src','/img/buttons/fullscreen.png') 59 | .addClass('window-button') 60 | .click(function () { self.emit('fullscreen') }) 61 | , 62 | $('') 63 | .attr('src','/img/buttons/minimize.png') 64 | .addClass('window-button') 65 | .click(function () { self.emit('minimize') }) 66 | , 67 | $('') 68 | .attr('src','/img/buttons/menu-down.png') 69 | .addClass('window-button') 70 | .addClass('menu-button') 71 | .toggle( 72 | function () { 73 | $(this).attr('src','/img/buttons/menu-up.png'); 74 | menu.fadeTo(250,0.95); 75 | }, 76 | function () { 77 | $(this).attr('src','/img/buttons/menu-down.png'); 78 | menu.fadeOut(250); 79 | } 80 | ) 81 | , 82 | $('
    ') 83 | .addClass('title-text') 84 | .text(proc.filename) 85 | .draggable({ 86 | appendTo : 'body', 87 | scroll : false, 88 | helper : function () { 89 | return $('
    ') 90 | .text(proc.filename) 91 | .addClass('title-text-drag') 92 | ; 93 | } 94 | }) 95 | .data('proc', proc) 96 | , 97 | menu 98 | ) 99 | ; 100 | } 101 | 102 | -------------------------------------------------------------------------------- /static/js/ui/workspace.js: -------------------------------------------------------------------------------- 1 | Workspace.prototype = new EventEmitter; 2 | function Workspace (params) { 3 | if (!(this instanceof Workspace)) return new Workspace(params); 4 | var self = this; 5 | 6 | var dragging = false; 7 | var lastPos = null; 8 | 9 | self.element = $('
    ') 10 | .attr('id','workspace') 11 | .mousedown(function (ev) { 12 | if ($(ev.target).attr('id') == 'workspace') { 13 | dragging = true; 14 | ev.preventDefault(); 15 | } 16 | }) 17 | .mouseup(function (ev) { 18 | if ($(ev.target).attr('id') == 'workspace') { 19 | dragging = false; 20 | ev.preventDefault(); 21 | } 22 | }) 23 | .mouseleave(function (ev) { dragging = false }) 24 | .mousemove(function (ev) { 25 | if (dragging && lastPos) { 26 | var delta = { 27 | x : ev.pageX - lastPos.x, 28 | y : ev.pageY - lastPos.y 29 | }; 30 | windows.forEach(function (win) { 31 | var pos = win.element.offset(); 32 | pos.left += delta.x; 33 | pos.top += delta.y; 34 | win.element.offset(pos); 35 | }); 36 | ev.preventDefault(); 37 | } 38 | lastPos = { x : ev.pageX, y : ev.pageY }; 39 | }) 40 | ; 41 | 42 | var windows = []; 43 | 44 | self.attachWindow = function (win) { 45 | win.titleBar.on('minimize', function () { 46 | // .. 47 | }); 48 | 49 | win.titleBar.on('fullscreen', function () { 50 | // .. 51 | }); 52 | 53 | win.titleBar.on('close', function () { 54 | self.detachWindow(win); 55 | }); 56 | 57 | win.titleBar.on('kill', function () { 58 | self.detachWindow(win); 59 | }); 60 | 61 | win.titleBar.on('restart', function () { 62 | // .. 63 | }); 64 | 65 | win.titleBar.on('screenshot', function () { 66 | win.proc.screenshot(); 67 | }); 68 | 69 | win.titleBar.on('screencastStart', function () { 70 | //self.emit('screencastStart'); 71 | }); 72 | 73 | win.titleBar.on('screencastStop', function () { 74 | //self.emit('screencastStop'); 75 | }); 76 | 77 | win.on('exit', function () { 78 | self.detachWindow(win); 79 | }); 80 | 81 | self.element.append(win.element); 82 | 83 | windows.push(win); 84 | }; 85 | 86 | self.detachWindow = function (win) { 87 | win.removeAllListeners(); 88 | win.element.remove(); 89 | var i = windows.indexOf(win); 90 | if (i >= 0) windows.splice(i,1); 91 | }; 92 | 93 | var chats = {}; 94 | 95 | self.hasChat = function (name) { 96 | return name in chats 97 | }; 98 | 99 | self.addChat = function (chat) { 100 | self.element.append(chat.element); 101 | 102 | var rightMost = Math.min.apply({}, [$(window).width()].concat( 103 | Object.keys(chats).map(function (name) { 104 | return chats[name].element.offset().left; 105 | }) 106 | )); 107 | chat.element.css('right', $(window).width() - rightMost + 10); 108 | 109 | chats[chat.contact.name] = chat; 110 | 111 | chat.on('close', function () { 112 | chat.element.remove(); 113 | delete chats[chat.contact.name]; 114 | }); 115 | 116 | chat.on('attach', function (vm) { 117 | self.emit('attach', vm); 118 | }); 119 | }; 120 | 121 | self.routeChat = function (contact, msg) { 122 | chats[contact.name].addMessage(contact.name, msg); 123 | }; 124 | 125 | self.routeResource = function (contact, type, res) { 126 | chats[contact.name].addResource(contact, type, res); 127 | }; 128 | } 129 | 130 | -------------------------------------------------------------------------------- /static/js/util/events.js: -------------------------------------------------------------------------------- 1 | // node.js-style EventEmitters for client-side javascript 2 | 3 | function EventEmitter () { 4 | if (!(this instanceof EventEmitter)) return new EventEmitter; 5 | } 6 | 7 | EventEmitter.prototype.listeners = function (name) { 8 | if (!this._events) this._events = {}; 9 | if (!(name in this._events)) this._events[name] = []; 10 | return this._events[name]; 11 | }; 12 | 13 | EventEmitter.prototype.emit = function (name) { 14 | var self = this; 15 | 16 | var args = [].slice.call(arguments,1); 17 | this.listeners(name).forEach(function (f) { 18 | f.apply(self,args); 19 | }); 20 | return this; 21 | }; 22 | 23 | EventEmitter.prototype.addListener = function (name, listener) { 24 | this.emit('newListener', name, listener); 25 | this.listeners(name).push(listener); 26 | return this; 27 | }; 28 | 29 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 30 | 31 | EventEmitter.prototype.removeListener = function (name, listener) { 32 | var i = this.listeners(name).find(listener); 33 | if (i >= 0) this.listeners(name).splice(i,1); 34 | return this; 35 | }; 36 | 37 | EventEmitter.prototype.removeAllListeners = function (name) { 38 | if (this._events && this._events[name]) this._events[name] = []; 39 | return this; 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /static/js/util/keymap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Guacamole - Pure JavaScript/HTML VNC Client 3 | * Copyright (C) 2010 Michael Jumper 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | var KeyMapper = (function() { 20 | var unshiftedKeySym = new Array(); 21 | unshiftedKeySym[8] = 0xFF08; // backspace 22 | unshiftedKeySym[9] = 0xFF09; // tab 23 | unshiftedKeySym[13] = 0xFF0D; // enter 24 | unshiftedKeySym[16] = 0xFFE1; // shift 25 | unshiftedKeySym[17] = 0xFFE3; // ctrl 26 | unshiftedKeySym[18] = 0xFFE9; // alt 27 | unshiftedKeySym[19] = 0xFF13; // pause/break 28 | unshiftedKeySym[20] = 0xFFE5; // caps lock 29 | unshiftedKeySym[27] = 0xFF1B; // escape 30 | unshiftedKeySym[32] = 0x20; // space 31 | unshiftedKeySym[33] = 0xFF55; // page up 32 | unshiftedKeySym[34] = 0xFF56; // page down 33 | unshiftedKeySym[35] = 0xFF57; // end 34 | unshiftedKeySym[36] = 0xFF50; // home 35 | unshiftedKeySym[37] = 0xFF51; // left arrow 36 | unshiftedKeySym[38] = 0xFF52; // up arrow 37 | unshiftedKeySym[39] = 0xFF53; // right arrow 38 | unshiftedKeySym[40] = 0xFF54; // down arrow 39 | unshiftedKeySym[45] = 0xFF63; // insert 40 | unshiftedKeySym[46] = 0xFFFF; // delete 41 | unshiftedKeySym[48] = 0x30; // '0' 42 | unshiftedKeySym[49] = 0x31; // '1' 43 | unshiftedKeySym[50] = 0x32; // '2' 44 | unshiftedKeySym[51] = 0x33; // '3' 45 | unshiftedKeySym[52] = 0x34; // '4' 46 | unshiftedKeySym[53] = 0x35; // '5' 47 | unshiftedKeySym[54] = 0x36; // '6' 48 | unshiftedKeySym[55] = 0x37; // '7' 49 | unshiftedKeySym[56] = 0x38; // '8' 50 | unshiftedKeySym[57] = 0x39; // '9' 51 | unshiftedKeySym[59] = 0x3B; // semi-colon 52 | unshiftedKeySym[61] = 0x3D; // equals sign 53 | unshiftedKeySym[65] = 0x61; // 'a' 54 | unshiftedKeySym[66] = 0x62; // 'b' 55 | unshiftedKeySym[67] = 0x63; // 'c' 56 | unshiftedKeySym[68] = 0x64; // 'd' 57 | unshiftedKeySym[69] = 0x65; // 'e' 58 | unshiftedKeySym[70] = 0x66; // 'f' 59 | unshiftedKeySym[71] = 0x67; // 'g' 60 | unshiftedKeySym[72] = 0x68; // 'h' 61 | unshiftedKeySym[73] = 0x69; // 'i' 62 | unshiftedKeySym[74] = 0x6A; // 'j' 63 | unshiftedKeySym[75] = 0x6B; // 'k' 64 | unshiftedKeySym[76] = 0x6C; // 'l' 65 | unshiftedKeySym[77] = 0x6D; // 'm' 66 | unshiftedKeySym[78] = 0x6E; // 'n' 67 | unshiftedKeySym[79] = 0x6F; // 'o' 68 | unshiftedKeySym[80] = 0x70; // 'p' 69 | unshiftedKeySym[81] = 0x71; // 'q' 70 | unshiftedKeySym[82] = 0x72; // 'r' 71 | unshiftedKeySym[83] = 0x73; // 's' 72 | unshiftedKeySym[84] = 0x74; // 't' 73 | unshiftedKeySym[85] = 0x75; // 'u' 74 | unshiftedKeySym[86] = 0x76; // 'v' 75 | unshiftedKeySym[87] = 0x77; // 'w' 76 | unshiftedKeySym[88] = 0x78; // 'x' 77 | unshiftedKeySym[89] = 0x79; // 'y' 78 | unshiftedKeySym[90] = 0x7A; // 'z' 79 | unshiftedKeySym[91] = 0xFFEB; // left window key (super_l) 80 | unshiftedKeySym[92] = 0xFF67; // right window key (menu key?) 81 | unshiftedKeySym[93] = null; // select key 82 | unshiftedKeySym[96] = 0x30; // numpad 0 83 | unshiftedKeySym[97] = 0x31; // numpad 1 84 | unshiftedKeySym[98] = 0x32; // numpad 2 85 | unshiftedKeySym[99] = 0x33; // numpad 3 86 | unshiftedKeySym[100] = 0x34; // numpad 4 87 | unshiftedKeySym[101] = 0x35; // numpad 5 88 | unshiftedKeySym[102] = 0x36; // numpad 6 89 | unshiftedKeySym[103] = 0x37; // numpad 7 90 | unshiftedKeySym[104] = 0x38; // numpad 8 91 | unshiftedKeySym[105] = 0x39; // numpad 9 92 | unshiftedKeySym[106] = 0x2A; // multiply 93 | unshiftedKeySym[107] = 0x3D; // equals 94 | unshiftedKeySym[109] = 0x2D; // subtract 95 | unshiftedKeySym[110] = 0x2E; // decimal point 96 | unshiftedKeySym[111] = 0x2F; // divide 97 | unshiftedKeySym[112] = 0xFFBE; // f1 98 | unshiftedKeySym[113] = 0xFFBF; // f2 99 | unshiftedKeySym[114] = 0xFFC0; // f3 100 | unshiftedKeySym[115] = 0xFFC1; // f4 101 | unshiftedKeySym[116] = 0xFFC2; // f5 102 | unshiftedKeySym[117] = 0xFFC3; // f6 103 | unshiftedKeySym[118] = 0xFFC4; // f7 104 | unshiftedKeySym[119] = 0xFFC5; // f8 105 | unshiftedKeySym[120] = 0xFFC6; // f9 106 | unshiftedKeySym[121] = 0xFFC7; // f10 107 | unshiftedKeySym[122] = 0xFFC8; // f11 108 | unshiftedKeySym[123] = 0xFFC9; // f12 109 | unshiftedKeySym[144] = 0xFF7F; // num lock 110 | unshiftedKeySym[145] = 0xFF14; // scroll lock 111 | unshiftedKeySym[186] = 0x3B; // semi-colon 112 | unshiftedKeySym[187] = 0x3D; // equal sign 113 | unshiftedKeySym[188] = 0x2C; // comma 114 | unshiftedKeySym[189] = 0x2D; // dash 115 | unshiftedKeySym[190] = 0x2E; // period 116 | unshiftedKeySym[191] = 0x2F; // forward slash 117 | unshiftedKeySym[192] = 0x60; // grave accent 118 | unshiftedKeySym[219] = 0x5B; // open bracket 119 | unshiftedKeySym[220] = 0x5C; // back slash 120 | unshiftedKeySym[221] = 0x5D; // close bracket 121 | unshiftedKeySym[222] = 0x27; // single quote 122 | 123 | 124 | // Shifted versions, IF DIFFERENT FROM UNSHIFTED! 125 | // If any of these are null, the unshifted one will be used. 126 | var shiftedKeySym = new Array(); 127 | shiftedKeySym[8] = null; // backspace 128 | shiftedKeySym[9] = null; // tab 129 | shiftedKeySym[13] = null; // enter 130 | shiftedKeySym[16] = null; // shift 131 | shiftedKeySym[17] = null; // ctrl 132 | shiftedKeySym[18] = 0xFFE7; // alt 133 | shiftedKeySym[19] = null; // pause/break 134 | shiftedKeySym[20] = null; // caps lock 135 | shiftedKeySym[27] = null; // escape 136 | shiftedKeySym[32] = null; // space 137 | shiftedKeySym[33] = null; // page up 138 | shiftedKeySym[34] = null; // page down 139 | shiftedKeySym[35] = null; // end 140 | shiftedKeySym[36] = null; // home 141 | shiftedKeySym[37] = null; // left arrow 142 | shiftedKeySym[38] = null; // up arrow 143 | shiftedKeySym[39] = null; // right arrow 144 | shiftedKeySym[40] = null; // down arrow 145 | shiftedKeySym[45] = null; // insert 146 | shiftedKeySym[46] = null; // delete 147 | shiftedKeySym[48] = 0x29; // ')' 148 | shiftedKeySym[49] = 0x21; // '!' 149 | shiftedKeySym[50] = 0x40; // '@' 150 | shiftedKeySym[51] = 0x23; // '#' 151 | shiftedKeySym[52] = 0x24; // '$' 152 | shiftedKeySym[53] = 0x25; // '%' 153 | shiftedKeySym[54] = 0x5E; // '^' 154 | shiftedKeySym[55] = 0x26; // '&' 155 | shiftedKeySym[56] = 0x2A; // '*' 156 | shiftedKeySym[57] = 0x28; // '(' 157 | shiftedKeySym[59] = 0x3A; // colon 158 | shiftedKeySym[61] = 0x2B; // plus sign 159 | shiftedKeySym[65] = 0x41; // 'A' 160 | shiftedKeySym[66] = 0x42; // 'B' 161 | shiftedKeySym[67] = 0x43; // 'C' 162 | shiftedKeySym[68] = 0x44; // 'D' 163 | shiftedKeySym[69] = 0x45; // 'E' 164 | shiftedKeySym[70] = 0x46; // 'F' 165 | shiftedKeySym[71] = 0x47; // 'G' 166 | shiftedKeySym[72] = 0x48; // 'H' 167 | shiftedKeySym[73] = 0x49; // 'I' 168 | shiftedKeySym[74] = 0x4A; // 'J' 169 | shiftedKeySym[75] = 0x4B; // 'K' 170 | shiftedKeySym[76] = 0x4C; // 'L' 171 | shiftedKeySym[77] = 0x4D; // 'M' 172 | shiftedKeySym[78] = 0x4E; // 'N' 173 | shiftedKeySym[79] = 0x4F; // 'O' 174 | shiftedKeySym[80] = 0x50; // 'P' 175 | shiftedKeySym[81] = 0x51; // 'Q' 176 | shiftedKeySym[82] = 0x52; // 'R' 177 | shiftedKeySym[83] = 0x53; // 'S' 178 | shiftedKeySym[84] = 0x54; // 'T' 179 | shiftedKeySym[85] = 0x55; // 'U' 180 | shiftedKeySym[86] = 0x56; // 'V' 181 | shiftedKeySym[87] = 0x57; // 'W' 182 | shiftedKeySym[88] = 0x58; // 'X' 183 | shiftedKeySym[89] = 0x59; // 'Y' 184 | shiftedKeySym[90] = 0x5A; // 'Z' 185 | shiftedKeySym[91] = null; // left window key 186 | shiftedKeySym[92] = null; // right window key 187 | shiftedKeySym[93] = null; // select key 188 | shiftedKeySym[96] = null; // numpad 0 189 | shiftedKeySym[97] = null; // numpad 1 190 | shiftedKeySym[98] = null; // numpad 2 191 | shiftedKeySym[99] = null; // numpad 3 192 | shiftedKeySym[100] = null; // numpad 4 193 | shiftedKeySym[101] = null; // numpad 5 194 | shiftedKeySym[102] = null; // numpad 6 195 | shiftedKeySym[103] = null; // numpad 7 196 | shiftedKeySym[104] = null; // numpad 8 197 | shiftedKeySym[105] = null; // numpad 9 198 | shiftedKeySym[106] = null; // multiply 199 | shiftedKeySym[107] = 0x2B; // add 200 | shiftedKeySym[109] = 0x5F; // subtract 201 | shiftedKeySym[110] = null; // decimal point 202 | shiftedKeySym[111] = null; // divide 203 | shiftedKeySym[112] = null; // f1 204 | shiftedKeySym[113] = null; // f2 205 | shiftedKeySym[114] = null; // f3 206 | shiftedKeySym[115] = null; // f4 207 | shiftedKeySym[116] = null; // f5 208 | shiftedKeySym[117] = null; // f6 209 | shiftedKeySym[118] = null; // f7 210 | shiftedKeySym[119] = null; // f8 211 | shiftedKeySym[120] = null; // f9 212 | shiftedKeySym[121] = null; // f10 213 | shiftedKeySym[122] = null; // f11 214 | shiftedKeySym[123] = null; // f12 215 | shiftedKeySym[144] = null; // num lock 216 | shiftedKeySym[145] = null; // scroll lock 217 | shiftedKeySym[186] = 0x3A; // colon 218 | shiftedKeySym[187] = 0x2B; // plus sign 219 | shiftedKeySym[188] = 0x3C; // less than 220 | shiftedKeySym[189] = 0x5F; // underscore 221 | shiftedKeySym[190] = 0x3E; // greater than 222 | shiftedKeySym[191] = 0x3F; // question mark 223 | shiftedKeySym[192] = 0x7E; // tilde 224 | shiftedKeySym[219] = 0x7B; // open brace 225 | shiftedKeySym[220] = 0x7C; // pipe 226 | shiftedKeySym[221] = 0x7D; // close brace 227 | shiftedKeySym[222] = 0x22; // double quote 228 | 229 | return { 230 | getKeySym: function(keycode, shift) { 231 | var table = shift ? shiftedKeySym : unshiftedKeySym; 232 | return table[keycode] || keycode; 233 | } 234 | }; 235 | 236 | })(); 237 | -------------------------------------------------------------------------------- /static/js/util/require.js: -------------------------------------------------------------------------------- 1 | var require = (function () { 2 | 3 | var required = {}; 4 | 5 | function require (src) { 6 | if (required[src]) return required[src]; 7 | 8 | var exports = {}; 9 | (function () { 10 | eval(getSync('/js/' + src + '.js')); 11 | }).call(exports); 12 | required[src] = exports; 13 | return exports; 14 | 15 | function getSync(uri) { 16 | var http = false; 17 | if (window.XMLHttpRequest) { 18 | http = new XMLHttpRequest(); 19 | } 20 | else { 21 | http = new ActiveXObject("Microsoft.XMLHTTP"); 22 | } 23 | if (http) { 24 | http.open('GET', uri, false); // false makes it synchronous 25 | http.send(); 26 | return http.responseText; 27 | } 28 | } 29 | } 30 | 31 | return require; 32 | 33 | })(); 34 | 35 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | StackVM 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 |
    19 | 20 |
    21 | <%- views.userbar({ user : user, tabs : tabs, request : request }) %> 22 |
    23 | 24 |
    25 | <%- body %> 26 |
    27 | 28 |
    29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /views/pages/index.html: -------------------------------------------------------------------------------- 1 | <% if (user) { %> 2 | click here to access the player 3 | 4 |
    5 | <% } %> 6 | -------------------------------------------------------------------------------- /views/player.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | stackVM 6 | 7 | 8 | <% stylesheets.forEach(function (href) { %> 9 | 10 | <% }) %> 11 | 12 | <% scripts.forEach(function (src) { %> 13 | 14 | <% }) %> 15 | 16 | 39 | 40 | 41 |
    42 | 43 | 44 | -------------------------------------------------------------------------------- /views/userbar.html: -------------------------------------------------------------------------------- 1 | <% if (user) { %> 2 | 3 | signed in as <%= user.name %> 4 | 5 |
    12 | 13 | <% } else { %> 14 |
    15 | 16 | 17 | 18 |
    19 | <% } %> 20 | --------------------------------------------------------------------------------