├── utils ├── wsproxy.py ├── Makefile ├── rebind ├── README.md ├── img2js.py ├── web.py ├── rebind.c ├── launch.sh ├── websockify └── websocket.py ├── favicon.ico ├── .gitignore ├── tests ├── face.png ├── base64.js ├── arrays.html ├── stats.js ├── input.html ├── face.png.js ├── base64.html ├── keyboard.html ├── vnc_playback.html ├── cursor.html ├── canvas.html ├── vnc_perf.html ├── json2graph.py └── arrays.js ├── images ├── favicon.ico ├── favicon.png ├── full_512x512.png └── screen_640x435.png ├── docs ├── rfbproto-3.3.pdf ├── rfbproto-3.7.pdf ├── rfbproto-3.8.pdf ├── flash_policy.txt ├── notes ├── TODO ├── links ├── rfb_notes └── LICENSE.LGPL-3 ├── include ├── web-socket-js │ ├── WebSocketMain.swf │ ├── README.txt │ ├── swfobject.js │ └── web_socket.js ├── vnc.js ├── plain.css ├── playback.js ├── black.css ├── webutil.js ├── base64.js ├── util.js ├── websock.js ├── des.js └── ui.js ├── LICENSE.txt ├── vnc.html ├── README.md └── vnc_auto.html /utils/wsproxy.py: -------------------------------------------------------------------------------- 1 | websockify -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- 1 | images/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.o 3 | wsproxy 4 | tests/data_*.js 5 | -------------------------------------------------------------------------------- /tests/face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/tests/face.png -------------------------------------------------------------------------------- /images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/images/favicon.ico -------------------------------------------------------------------------------- /images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/images/favicon.png -------------------------------------------------------------------------------- /docs/rfbproto-3.3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/docs/rfbproto-3.3.pdf -------------------------------------------------------------------------------- /docs/rfbproto-3.7.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/docs/rfbproto-3.7.pdf -------------------------------------------------------------------------------- /docs/rfbproto-3.8.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/docs/rfbproto-3.8.pdf -------------------------------------------------------------------------------- /images/full_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/images/full_512x512.png -------------------------------------------------------------------------------- /images/screen_640x435.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/images/screen_640x435.png -------------------------------------------------------------------------------- /include/web-socket-js/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Todo/noVNC/master/include/web-socket-js/WebSocketMain.swf -------------------------------------------------------------------------------- /utils/Makefile: -------------------------------------------------------------------------------- 1 | TARGETS=rebind.so 2 | CFLAGS += -fPIC 3 | 4 | all: $(TARGETS) 5 | 6 | rebind.so: rebind.o 7 | $(CC) $(LDFLAGS) $^ -shared -fPIC -ldl -o $@ 8 | 9 | clean: 10 | rm -f rebind.o rebind.so 11 | 12 | -------------------------------------------------------------------------------- /docs/flash_policy.txt: -------------------------------------------------------------------------------- 1 | Manual setup: 2 | 3 | DATA="echo \'\'" 4 | /usr/bin/socat -T 1 TCP-L:843,reuseaddr,fork,crlf SYSTEM:"$DATA" 5 | -------------------------------------------------------------------------------- /tests/base64.js: -------------------------------------------------------------------------------- 1 | // The following results in 'hello [MANGLED]' 2 | // 3 | // Filed as https://github.com/ry/node/issues/issue/402 4 | 5 | var sys = require("sys"), 6 | buf = new Buffer(1024), len, 7 | str1 = "aGVsbG8g", // 'hello ' 8 | str2 = "d29ybGQ=", // 'world' 9 | 10 | len = buf.write(str1, 0, 'base64'); 11 | len += buf.write(str2, len, 'base64'); 12 | sys.log("decoded result: " + buf.toString('binary', 0, len)); 13 | -------------------------------------------------------------------------------- /utils/rebind: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage() { 4 | echo "Usage: $(basename $0) OLD_PORT NEW_PORT COMMAND_LINE" 5 | echo 6 | echo "Launch COMMAND_LINE, but intercept system calls to bind" 7 | echo "to OLD_PORT and instead bind them to localhost:NEW_PORT" 8 | exit 2 9 | } 10 | 11 | # Parameter defaults 12 | mydir=$(readlink -f $(dirname ${0})) 13 | 14 | export REBIND_PORT_OLD="${1}"; shift 15 | export REBIND_PORT_NEW="${1}"; shift 16 | 17 | LD_PRELOAD=${mydir}/rebind.so "${@}" 18 | 19 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | ## WebSockets Proxy 2 | 3 | wsproxy has become [websockify](https://github.com/kanaka/websockify). 4 | A copy of the python version of websockify (named wsproxy.py) is kept 5 | here for ease of use. The other versions of websockify (C, Node.js) 6 | and the associated test programs have been moved to 7 | [websockify](https://github.com/kanaka/websockify). 8 | 9 | For more detailed description and usage information please refer to 10 | the [websockify README](https://github.com/kanaka/websockify/blob/master/README.md). 11 | 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | noVNC is licensed under the LGPL version 3 (see docs/LICENSE.GPL-3 and 2 | docs/LICENSE.LGPL-3) with the following exceptions: 3 | 4 | include/base64.js : Dual GPL-2 or LGPL-2.1 5 | 6 | incluee/des.js : Various BSD style licenses 7 | 8 | include/web-socket-js/ : New BSD license. Source code at 9 | http://github.com/gimite/web-socket-js 10 | 11 | images/ : Creative Commons Attribution-ShareAlike 12 | http://creativecommons.org/licenses/by-sa/3.0/ 13 | Uses of the work must be attributed 14 | to the noVNC project. 15 | -------------------------------------------------------------------------------- /docs/notes: -------------------------------------------------------------------------------- 1 | Some implementation notes: 2 | 3 | There is an included flash object (web-socket-js) that is used to 4 | emulate websocket support on browsers without websocket support 5 | (currently only Chrome has WebSocket support). 6 | 7 | Javascript doesn't have a bytearray type, so what you get out of 8 | a WebSocket object is just Javascript strings. Javascript has UTF-16 9 | unicode strings and anything sent through the WebSocket gets converted 10 | to UTF-8 and vice-versa. So, one additional (and necessary) function 11 | of wsproxy is base64 encoding/decoding what is sent to/from the 12 | browser. 13 | 14 | Building web-socket-js emulator: 15 | 16 | cd include/web-socket-js/flash-src 17 | mxmlc -static-link-runtime-shared-libraries WebSocketMain.as 18 | -------------------------------------------------------------------------------- /vnc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | noVNC 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 |
Loading
23 | 24 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /utils/img2js.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # 4 | # Convert image to Javascript compatible base64 Data URI 5 | # Copyright 2011 Joel Martin 6 | # Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) 7 | # 8 | 9 | import sys, base64 10 | 11 | try: 12 | from PIL import Image 13 | except: 14 | print "python PIL module required (python-imaging package)" 15 | sys.exit(1) 16 | 17 | 18 | if len(sys.argv) < 3: 19 | print "Usage: %s IMAGE JS_VARIABLE" % sys.argv[0] 20 | sys.exit(1) 21 | 22 | fname = sys.argv[1] 23 | var = sys.argv[2] 24 | 25 | ext = fname.lower().split('.')[-1] 26 | if ext == "png": mime = "image/png" 27 | elif ext in ["jpg", "jpeg"]: mime = "image/jpeg" 28 | elif ext == "gif": mime = "image/gif" 29 | else: 30 | print "Only PNG, JPEG and GIF images are supported" 31 | sys.exit(1) 32 | uri = "data:%s;base64," % mime 33 | 34 | im = Image.open(fname) 35 | w, h = im.size 36 | 37 | raw = open(fname).read() 38 | 39 | print '%s = {"width": %s, "height": %s, "data": "%s%s"};' % ( 40 | var, w, h, uri, base64.b64encode(raw)) 41 | -------------------------------------------------------------------------------- /include/vnc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * noVNC: HTML5 VNC client 3 | * Copyright (C) 2010 Joel Martin 4 | * Licensed under LGPL-3 (see LICENSE.txt) 5 | * 6 | * See README.md for usage and integration instructions. 7 | */ 8 | 9 | "use strict"; 10 | /*jslint evil: true */ 11 | /*global window, document, INCLUDE_URI */ 12 | 13 | /* 14 | * Load supporting scripts 15 | */ 16 | function get_INCLUDE_URI() { 17 | return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/"; 18 | } 19 | 20 | (function () { 21 | var extra = "", start, end; 22 | 23 | start = " 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Javascript Arrays Performance Test

17 | Iterations:   18 | Array Size: *1024  19 | 20 |   22 | 23 |

24 | Results:
25 | 26 |
27 | 28 | 29 | 30 | 31 | 39 | 40 | -------------------------------------------------------------------------------- /tests/stats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Define some useful statistical functions on arrays of numbers 3 | */ 4 | 5 | Array.prototype.sum = function() { 6 | var i, sum = 0; 7 | for (i = 0; i < this.length; i++) { 8 | sum += this[i]; 9 | } 10 | return sum; 11 | } 12 | 13 | Array.prototype.max = function() { 14 | return Math.max.apply(null, this); 15 | } 16 | 17 | Array.prototype.min = function() { 18 | return Math.min.apply(null, this); 19 | } 20 | 21 | Array.prototype.mean = function() { 22 | return this.sum() / this.length; 23 | } 24 | Array.prototype.average = Array.prototype.mean; 25 | 26 | Array.prototype.median = function() { 27 | var sorted = this.sort( function(a,b) { return a-b; }), 28 | len = sorted.length; 29 | if (len % 2) { 30 | return sorted[Math.floor(len / 2)]; // Odd 31 | } else { 32 | return (sorted[len/2 - 1] + sorted[len/2]) / 2; // Even 33 | } 34 | } 35 | 36 | Array.prototype.stdDev = function(sample) { 37 | var i, sumSqr = 0, mean = this.mean(), N; 38 | 39 | if (sample) { 40 | // Population correction if this is a sample 41 | N = this.length - 1; 42 | } else { 43 | // Standard deviation of just the array 44 | N = this.length; 45 | } 46 | 47 | for (i = 0; i < this.length; i++) { 48 | sumSqr += Math.pow(this[i] - mean, 2); 49 | } 50 | 51 | return Math.sqrt(sumSqr / N); 52 | } 53 | 54 | -------------------------------------------------------------------------------- /docs/TODO: -------------------------------------------------------------------------------- 1 | Short Term: 2 | 3 | - Test normal arrays vs TypedArrays vs imageData arrays. 4 | - Blog post about it. 5 | 6 | - VNC performance and regression playback suite. 7 | - WebSockets 8 | - expand latency test 9 | - add absolute timers (every 500 packets) 10 | - try 1 ms delay 11 | - stop at 4000 packets 12 | - small and large packets test 13 | 14 | - JavaScript 15 | - just base64 decode 16 | - everything except Canvas 17 | 18 | - Full test 19 | - Without WebSockets 20 | - With replay from python tester 21 | - add higher-resolution multi test 22 | 23 | - websockify test with echo and playback functionality 24 | - choosen by client test page on connect 25 | 26 | 27 | - Keyboard layout/internationalization support 28 | - convert keyCode into proper charCode 29 | 30 | - IE 9 improvements. 31 | - https://github.com/gimite/web-socket-js/issues#issue/41 32 | - try window.atob instead of decode for better performance 33 | 34 | - Status bar menu/buttons: 35 | - Explanatory hover text over buttons 36 | 37 | - Configuration menu: 38 | - Tunable: speed vs. bandwidth selection 39 | - Tunable: CPU use versus latency. 40 | - Scaling 41 | 42 | - Keyboard menu: 43 | - Ctrl Lock, Alt Lock, SysRq Lock 44 | - Highlight menu icon when keys are locked 45 | 46 | - Clipboard button -> popup: 47 | - text, clear and send buttons 48 | 49 | Medium Term: 50 | 51 | - Viewport support 52 | 53 | - Touchscreen testing/support. 54 | -------------------------------------------------------------------------------- /utils/web.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | A super simple HTTP/HTTPS webserver for python. Automatically detect 4 | 5 | You can make a cert/key with openssl using: 6 | openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem 7 | as taken from http://docs.python.org/dev/library/ssl.html#certificates 8 | 9 | ''' 10 | 11 | import traceback, sys 12 | import socket 13 | import ssl 14 | #import http.server as server # python 3.X 15 | import SimpleHTTPServer as server # python 2.X 16 | 17 | def do_request(connstream, from_addr): 18 | x = object() 19 | server.SimpleHTTPRequestHandler(connstream, from_addr, x) 20 | connstream.close() 21 | 22 | def serve(): 23 | bindsocket = socket.socket() 24 | bindsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 25 | #bindsocket.bind(('localhost', PORT)) 26 | bindsocket.bind(('', PORT)) 27 | bindsocket.listen(5) 28 | 29 | print("serving on port", PORT) 30 | 31 | while True: 32 | try: 33 | newsocket, from_addr = bindsocket.accept() 34 | peek = newsocket.recv(1024, socket.MSG_PEEK) 35 | if peek.startswith("\x16"): 36 | connstream = ssl.wrap_socket( 37 | newsocket, 38 | server_side=True, 39 | certfile='self.pem', 40 | ssl_version=ssl.PROTOCOL_TLSv1) 41 | else: 42 | connstream = newsocket 43 | 44 | do_request(connstream, from_addr) 45 | 46 | except Exception: 47 | traceback.print_exc() 48 | 49 | try: 50 | PORT = int(sys.argv[1]) 51 | except: 52 | print "%s port" % sys.argv[0] 53 | sys.exit(2) 54 | 55 | serve() 56 | -------------------------------------------------------------------------------- /include/plain.css: -------------------------------------------------------------------------------- 1 | #VNC_controls { 2 | overflow: hidden; 3 | } 4 | #VNC_controls ul { 5 | list-style: none; 6 | margin: 0px; 7 | padding: 0px; 8 | } 9 | #VNC_controls li { 10 | float: left; 11 | margin-right: 15px; 12 | } 13 | 14 | #VNC_host { 15 | width: 100px; 16 | } 17 | #VNC_port { 18 | width: 50px; 19 | } 20 | #VNC_password { 21 | width: 80px; 22 | } 23 | #VNC_encrypt { 24 | } 25 | #VNC_connectTimeout { 26 | width: 30px; 27 | } 28 | #VNC_connect_button { 29 | width: 110px; 30 | } 31 | 32 | #VNC_status_bar td { 33 | margin-top: 15px; 34 | padding: 0px; 35 | margin: 0px; 36 | } 37 | #VNC_status_bar div { 38 | font-size: 12px; 39 | margin: 0px; 40 | padding: 0px; 41 | } 42 | .VNC_status_button { 43 | font-size: 10px; 44 | margin: 0px; 45 | padding: 0px; 46 | } 47 | #VNC_status { 48 | text-align: center; 49 | } 50 | #VNC_settings_menu { 51 | display: none; 52 | position: absolute; 53 | width: 12em; 54 | border: 1px solid #888; 55 | background-color: #f0f2f6; 56 | padding: 5px; margin: 3px; 57 | z-index: 100; opacity: 1; 58 | text-align: left; white-space: normal; 59 | } 60 | #VNC_settings_menu ul { 61 | list-style: none; 62 | margin: 0px; 63 | padding: 0px; 64 | } 65 | 66 | .VNC_buttons_right { 67 | text-align: right; 68 | } 69 | .VNC_buttons_left { 70 | text-align: left; 71 | } 72 | .VNC_status_normal { 73 | background: #eee; 74 | } 75 | .VNC_status_error { 76 | background: #f44; 77 | } 78 | .VNC_status_warn { 79 | background: #ff4; 80 | } 81 | 82 | /* Do not set width/height for VNC_screen or VNC_canvas or incorrect 83 | * scaling will occur. Canvas resizes to remote VNC settings */ 84 | #VNC_screen { 85 | text-align: center; 86 | display: table; 87 | } 88 | #VNC_canvas { 89 | background: #eee; 90 | } 91 | 92 | #VNC_clipboard_clear_button { 93 | } 94 | #VNC_clipboard_text { 95 | font-size: 9px; 96 | } 97 | 98 | -------------------------------------------------------------------------------- /tests/input.html: -------------------------------------------------------------------------------- 1 | 2 | Input Test 3 | 4 |

5 | 6 | Canvas:
7 | 9 | Canvas not supported. 10 | 11 | 12 |
13 | Results:
14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 62 | 63 | -------------------------------------------------------------------------------- /include/playback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * noVNC: HTML5 VNC client 3 | * Copyright (C) 2010 Joel Martin 4 | * Licensed under LGPL-3 (see LICENSE.LGPL-3) 5 | */ 6 | 7 | "use strict"; 8 | /*jslint browser: true, white: false */ 9 | /*global Util, VNC_frame_data, finish */ 10 | 11 | var rfb, mode, test_state, frame_idx, frame_length, 12 | iteration, iterations, istart_time, 13 | 14 | // Pre-declarations for jslint 15 | send_array, next_iteration, queue_next_packet, do_packet; 16 | 17 | // Override send_array 18 | send_array = function (arr) { 19 | // Stub out send_array 20 | }; 21 | 22 | next_iteration = function () { 23 | if (iteration === 0) { 24 | frame_length = VNC_frame_data.length; 25 | test_state = 'running'; 26 | } else { 27 | rfb.disconnect(); 28 | } 29 | 30 | if (test_state !== 'running') { return; } 31 | 32 | iteration += 1; 33 | if (iteration > iterations) { 34 | finish(); 35 | return; 36 | } 37 | 38 | frame_idx = 0; 39 | istart_time = (new Date()).getTime(); 40 | rfb.connect('test', 0, "bogus"); 41 | 42 | queue_next_packet(); 43 | 44 | }; 45 | 46 | queue_next_packet = function () { 47 | var frame, foffset, toffset, delay; 48 | if (test_state !== 'running') { return; } 49 | 50 | frame = VNC_frame_data[frame_idx]; 51 | while ((frame_idx < frame_length) && (frame.charAt(0) === "}")) { 52 | //Util.Debug("Send frame " + frame_idx); 53 | frame_idx += 1; 54 | frame = VNC_frame_data[frame_idx]; 55 | } 56 | 57 | if (frame === 'EOF') { 58 | Util.Debug("Finished, found EOF"); 59 | next_iteration(); 60 | return; 61 | } 62 | if (frame_idx >= frame_length) { 63 | Util.Debug("Finished, no more frames"); 64 | next_iteration(); 65 | return; 66 | } 67 | 68 | if (mode === 'realtime') { 69 | foffset = frame.slice(1, frame.indexOf('{', 1)); 70 | toffset = (new Date()).getTime() - istart_time; 71 | delay = foffset - toffset; 72 | if (delay < 1) { 73 | delay = 1; 74 | } 75 | 76 | setTimeout(do_packet, delay); 77 | } else { 78 | setTimeout(do_packet, 1); 79 | } 80 | }; 81 | 82 | do_packet = function () { 83 | //Util.Debug("Processing frame: " + frame_idx); 84 | var frame = VNC_frame_data[frame_idx]; 85 | rfb.recv_message({'data' : frame.slice(frame.indexOf('{', 1) + 1)}); 86 | frame_idx += 1; 87 | 88 | queue_next_packet(); 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /docs/links: -------------------------------------------------------------------------------- 1 | New tight PNG protocol: 2 | http://wiki.qemu.org/VNC_Tight_PNG 3 | http://xf.iksaif.net/blog/index.php?post/2010/06/14/QEMU:-Tight-PNG-and-some-profiling 4 | 5 | RFB protocol and extensions: 6 | http://tigervnc.org/cgi-bin/rfbproto 7 | 8 | Canvas Browser Compatibility: 9 | http://philip.html5.org/tests/canvas/suite/tests/results.html 10 | 11 | WebSockets API standard: 12 | http://www.whatwg.org/specs/web-apps/current-work/complete.html#websocket 13 | http://dev.w3.org/html5/websockets/ 14 | http://www.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-00.txt 15 | 16 | Browser Keyboard Events detailed: 17 | http://unixpapa.com/js/key.html 18 | 19 | ActionScript (Flash) WebSocket implementation: 20 | http://github.com/gimite/web-socket-js 21 | 22 | ActionScript (Flash) crypto/TLS library: 23 | http://code.google.com/p/as3crypto 24 | http://github.com/lyokato/as3crypto_patched 25 | 26 | TLS Protocol: 27 | http://en.wikipedia.org/wiki/Transport_Layer_Security 28 | 29 | Generate self-signed certificate: 30 | http://docs.python.org/dev/library/ssl.html#certificates 31 | 32 | Cursor appearance/style (for Cursor pseudo-encoding): 33 | http://en.wikipedia.org/wiki/ICO_(file_format) 34 | http://www.daubnet.com/en/file-format-cur 35 | https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property 36 | http://www.fileformat.info/format/bmp/egff.htm 37 | 38 | Icon/Cursor file format: 39 | http://msdn.microsoft.com/en-us/library/ms997538 40 | http://msdn.microsoft.com/en-us/library/aa921550.aspx 41 | http://msdn.microsoft.com/en-us/library/aa930622.aspx 42 | 43 | 44 | RDP Protocol specification: 45 | http://msdn.microsoft.com/en-us/library/cc240445(v=PROT.10).aspx 46 | 47 | 48 | Related projects: 49 | 50 | guacamole: http://guacamole.sourceforge.net/ 51 | 52 | - Web client, but Java servlet does pre-processing 53 | 54 | jsvnc: http://code.google.com/p/jsvnc/ 55 | 56 | - No releases 57 | 58 | webvnc: http://code.google.com/p/webvnc/ 59 | 60 | - Jetty web server gateway, no updates since April 2008. 61 | 62 | RealVNC Java applet: http://www.realvnc.com/support/javavncviewer.html 63 | 64 | - Java applet 65 | 66 | Flashlight-VNC: http://www.wizhelp.com/flashlight-vnc/ 67 | 68 | - Adobe Flash implementation 69 | 70 | FVNC: http://osflash.org/fvnc 71 | 72 | - Adbove Flash implementation 73 | 74 | CanVNC: http://canvnc.sourceforge.net/ 75 | 76 | - HTML client with REST to VNC python proxy. Mostly vapor. 77 | -------------------------------------------------------------------------------- /include/black.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | font-size: 13px; 4 | color: #111; 5 | font-family: "Helvetica"; 6 | } 7 | 8 | #VNC_controls { 9 | background: #111; 10 | line-height: 1em; 11 | color: #FFF; 12 | overflow: hidden; 13 | padding: 4px 24px; 14 | } 15 | 16 | #VNC_controls ul { 17 | list-style:none; 18 | list-style-position: outside; 19 | margin: 0px; 20 | padding: 0px; 21 | } 22 | #VNC_controls li { 23 | margin-right: 15px; 24 | padding: 2px 0px; 25 | float: left; 26 | } 27 | #VNC_controls li input[type=text], 28 | #VNC_controls li input[type=password] { 29 | border: 2px solid #333; 30 | } 31 | 32 | #VNC_host { 33 | width: 100px; 34 | } 35 | #VNC_port { 36 | width: 50px; 37 | } 38 | #VNC_password { 39 | width: 80px; 40 | } 41 | #VNC_encrypt { 42 | } 43 | #VNC_connect_button { 44 | width: 100px; 45 | } 46 | 47 | #VNC_status_bar td { 48 | padding: 0px; 49 | margin: 0px; 50 | } 51 | #VNC_status_bar div { 52 | font-size: 12px; 53 | font-weight: bold; 54 | text-align: center; 55 | margin: 0px; 56 | padding: 1em; 57 | } 58 | .VNC_status_button { 59 | font-size: 10px; 60 | margin: 0px; 61 | padding: 0px; 62 | } 63 | #VNC_status { 64 | text-align: center; 65 | } 66 | #VNC_settings_menu { 67 | display: none; 68 | position: absolute; 69 | width: 13em; 70 | border: 1px solid #888; 71 | color: #111; 72 | font-weight: normal; 73 | background-color: #f0f2f6; 74 | padding: 5px; margin: 3px; 75 | z-index: 100; opacity: 1; 76 | text-align: left; white-space: normal; 77 | } 78 | #VNC_settings_menu ul { 79 | list-style: none; 80 | margin: 0px; 81 | padding: 0px; 82 | } 83 | 84 | .VNC_buttons_right { 85 | text-align: right; 86 | } 87 | .VNC_buttons_left { 88 | text-align: left; 89 | } 90 | .VNC_status_normal { 91 | background: #111; 92 | color: #fff; 93 | } 94 | .VNC_status_error { 95 | background: #111; 96 | color: #f44; 97 | } 98 | .VNC_status_warn { 99 | background: #111; 100 | color: #ff4; 101 | } 102 | 103 | #VNC_screen { 104 | -webkit-border-radius: 10px; 105 | -moz-border-radius: 10px; 106 | border-radius: 10px; 107 | background: #111; 108 | padding: 20px; 109 | margin: 0 auto; 110 | color: #FFF; 111 | margin-top: 20px; 112 | text-align: center; 113 | 114 | /* This causes the width of the outer div(#screen) honor the size of the inner (#vnc) div */ 115 | display: table; 116 | table-layout: auto; 117 | } 118 | #VNC_canvas { 119 | background: #111; 120 | margin: 0 auto; 121 | } 122 | #VNC_clipboard { 123 | display: none; 124 | } 125 | -------------------------------------------------------------------------------- /tests/face.png.js: -------------------------------------------------------------------------------- 1 | var face64 = 'iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAIAAACRuyQOAAAAA3NCSVQICAjb4U/gAAAAGXRFWHRTb2Z0d2FyZQBnbm9tZS1zY3JlZW5zaG907wO/PgAACJJJREFUSIm1lltsXMUdxr8558zZq9d3OxebJDYhJLhNIAmUWyFKIBUtVaGqSgtUlIJKeahoEahgIZU+VC0oQiVVC60obckDgVIp3KRCQkmhhIhA4oY4wjg2ufmS9drec/bc5vbvw9prJwq85dP/YWfP7Pfb/8w3s8v6339l2fkrbMvGuZQ2mkUTA0bpc4qpyjrX3dTkAATQ5z0WUrqcAwjL/eXirmBqj0yKSTTBwNxMM0+15JuurG/dlClcOH/yWcVEaVBKUR3Eidizr2946Nhr/9q5b//BsudZzDLG5DK4sDt3443XrFm34bkX9x4ZPimkWNBa/+MfrB84+O7rbxz4+JPQD8liljY6n8t9uWfld2/++vp1F3ct6cikU2eSnvr7P7e99OqC9vaTJ0ccMtl8loyJ4igKwzAIK0GglersWv7sM08VCrk4joY/O/rLXz3mTYzmcnnXdZXWcRzHURwEQRCEHUuXdS/vnp4qP/CT2zdvuAKAQwCB4kRse+m1LY//Wojkscd/57opKUQUJ8wyzFaOq7OGGGPcdZ/f/sKbu3YT0YZrr3JT7pq1l3qeH4SBqgRETBljDKXSqXyh/i9PP/W/Q31btz59zVXrUpxb1dYsixUK+c7Fi59/YUdz2yInnbXcLHfTtpu23ZRlu4ZZiRBTp8Z37HjlhW1/evnFZ9/a+VZdLsecFOMpx83ydJanc24q67iuFOr48NC1G6+fKBY7zutIElFNBAC4nN99602XXLzutjvvETqAlcqktVQin0QiLsRxEAUBaRVUfBh1QfcigmzIuw0NTe2LOjNlL07iOArDwA88z0unGWNTk5P1dfkf3XH3BT2r9b23zZKIAHxr81f/uGpF/8G+Fau+VPbKp8ZHpqdKSRiEYWiMMVopJSuVyl+f3UpIQKL34btvvf2BxuZWN5Umo7TWFiNDDHCampob6utLpRKz7Hvv+E5jfR5ELCkNShFXOytOTH7vjrsOfXJ0wcLFF63sXr1mfXtbS6FQB4BZyGYzX7l0TWtrvWVpUGxUMFEa2bv3Q9+bNCaECX2/NFEc3bd/4r19/tR0uLC98c+/3/LVy9fWzhNq56m1pfEPvabnut2OI8EvBMAYAxhgAWz3u3tuvuWeRx/56aYNa0Hy3fc/euiRZx596IZvbF5Dpgw9CdMI0waqaMrEScPgvtdWXH5JzdzC7NElIPQH3GyTk+4ABCgCEpAkMgRGcLb/49WGxqYtTzwNaJDa/tJ7DU1tW558GaYCEwESYGAWwEidTOcWM8tElcGauTP/ivDGd7V3fxv6JGCBIpBDjIMxgIM5B/YfjMJwfGwEMIA40DcQhcn46DGAzX7p6gIwBhj5WUvH8vLYG+nu8+d6qimY2lPXup70GFEEE9baAhRIj5w8cfz4MSESkJw3FLOfnrvSCETqs3xTd2Vyd+1Na/4MmRRt3gBTgfGJKkQhTAQTwgQgv2tpR8X3Vq5YCiiC7lrSXPG9lRe0AmZ2hQxo5jXpspNqEElxPmlOIi5ZThYUgBKYKRgPxgMFMAGM/+D9P2xuLPQ+dBcoAYkHf/bN5sZM74M3gHS1acBUi0gZ4zk8J5NyzdzBGSIJkoANCqsrwgBAg+zN1605Mfw6IIkiUHL9xouODzwBE4ACkKrGBNBkBEgSKSIz39gxRkuRVAduulHLCZtZoARkzybTAFU2m7GjBBSDkmoRJYCc3U5lSBgjAFeJae4Wauan9WSnWlU0aqdtUAXElAicVDNIgfHZaJkZU0pAESgmCJAACUCApJIBKCITg+VVMuWm2+btEwFE1coVLvOKe2HVE8UwUd/OXi0nQZXZ8kH+7HIFoIgoqvKqzWkV9L2zy5jQ6Ig5nX5pOFd/Vc3cmv9zW9eyYfzITmY1giKiMJNtCiYPw1RgPBh/psiHqcAEZAJQBFMlxaDEnyqmc3mjY2NCiy+bHB3Kt2w8I+UzxTPLlAzjygCz6kFBx6qNg/ue84p9M7AZRoWoQhSAqumfacsrnRg6uH9Rd4/RFWafl1RGjLJ5ZknNnIXjh+PQB0BEQkqv9L4sb1t59cMU74GVKxcnhg5sdzN1jQtX5grtqVyj46ZtywIJrUOZeCKYCLxTU+PHkzhZ2vO1XH5MRIfcwvcHP9qRafp5XfN6l3PGGIA5ktJaJEJINXnkvmWrNza0rSBxEFYbnE6veGRq9IPQO54Ep5QItRYAs22Hu1k315QtdDYsuCzf1KHDt0XlbTu3ySuVRo6MNnc/6XLHTbmObc+QotAHIJUSQiSJTKLR4Nh9Pdc+kM44JA+D5RhfBud8ZjeD5WHVMVYHqwAYmGkyUyRPqPDfMnhTxcNW+jKpGj/94NX8eVtTmYWpFHddlzsOABaOzZGkkImQUsrI/1iVfrPq6vszuSyJD0EasGEVmN0KlgXLgYGMT6qkkwEthrQuG53Y2U0icT79YIfb2pup6+Gcp1zOXV4j9VdJxhghpJBSSCmEjL0+XXqsa+0tTYvWQ/aTHJrZW9JEkowwJjYmMjo0OmR8uZ1eNz12+Nih/zgtv0gXVrsur1Jcl1uWNUsK/GoQldZSSCGllEpIGYcndOm36Vyqa/VNmboFRh4ldZR02ZhpMhJwCGnmLGZ8SewXj/bvTkLDW3pT2UUu55w7Lufc5dVNAsCCsf4o8Gqpr8KkUlIqpZRUKim/Y/y/pVLZ1s5V+Zbl3C3Ybp5Iq2RKxhP+xFBxZFAmwi7cmaq/kjuO4zicO9xx5mPOQqrGvYZRWmulldYqGlLBf3X8EfQkSR8A43WMN1nuWid3hZPpcmzbdmzHtmuwarjnkw5FldNIczyljDZKa62NNpoM1QSA1WQx27Jt23Js27It7pzJmLthz/7/nzHOOThcImPoNBIIAMNpJMtiNcBZDZ3PfVIjgtkWsy3riyZ9AaFGMlozhuqCnDsxxv4PC7uS+QV5eeoAAAAASUVORK5CYII='; 2 | -------------------------------------------------------------------------------- /utils/rebind.c: -------------------------------------------------------------------------------- 1 | /* 2 | * rebind: Intercept bind calls and bind to a different port 3 | * Copyright 2010 Joel Martin 4 | * Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) 5 | * 6 | * Overload (LD_PRELOAD) bind system call. If REBIND_PORT_OLD and 7 | * REBIND_PORT_NEW environment variables are set then bind on the new 8 | * port (of localhost) instead of the old port. 9 | * 10 | * This allows a proxy (such as wsproxy) to run on the old port and translate 11 | * traffic to/from the new port. 12 | * 13 | * Usage: 14 | * LD_PRELOAD=./rebind.so \ 15 | * REBIND_PORT_OLD=23 \ 16 | * REBIND_PORT_NEW=2023 \ 17 | * program 18 | */ 19 | 20 | //#define DO_DEBUG 1 21 | 22 | #include 23 | #include 24 | 25 | #define __USE_GNU 1 // Pull in RTLD_NEXT 26 | #include 27 | 28 | #include 29 | #include 30 | 31 | 32 | #if defined(DO_DEBUG) 33 | #define DEBUG(...) \ 34 | fprintf(stderr, "wswrapper: "); \ 35 | fprintf(stderr, __VA_ARGS__); 36 | #else 37 | #define DEBUG(...) 38 | #endif 39 | 40 | 41 | int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 42 | { 43 | static void * (*func)(); 44 | int do_move = 0; 45 | struct sockaddr_in * addr_in = (struct sockaddr_in *)addr; 46 | struct sockaddr_in addr_tmp; 47 | socklen_t addrlen_tmp; 48 | char * PORT_OLD, * PORT_NEW, * end1, * end2; 49 | int ret, oldport, newport, askport = htons(addr_in->sin_port); 50 | uint32_t askaddr = htons(addr_in->sin_addr.s_addr); 51 | if (!func) func = (void *(*)()) dlsym(RTLD_NEXT, "bind"); 52 | 53 | DEBUG(">> bind(%d, _, %d), askaddr %d, askport %d\n", 54 | sockfd, addrlen, askaddr, askport); 55 | 56 | /* Determine if we should move this socket */ 57 | if (addr_in->sin_family == AF_INET) { 58 | // TODO: support IPv6 59 | PORT_OLD = getenv("REBIND_OLD_PORT"); 60 | PORT_NEW = getenv("REBIND_NEW_PORT"); 61 | if (PORT_OLD && (*PORT_OLD != '\0') && 62 | PORT_NEW && (*PORT_NEW != '\0')) { 63 | oldport = strtol(PORT_OLD, &end1, 10); 64 | newport = strtol(PORT_NEW, &end2, 10); 65 | if (oldport && (*end1 == '\0') && 66 | newport && (*end2 == '\0') && 67 | (oldport == askport)) { 68 | do_move = 1; 69 | } 70 | } 71 | } 72 | 73 | if (! do_move) { 74 | /* Just pass everything right through to the real bind */ 75 | ret = (int) func(sockfd, addr, addrlen); 76 | DEBUG("<< bind(%d, _, %d) ret %d\n", sockfd, addrlen, ret); 77 | return ret; 78 | } 79 | 80 | DEBUG("binding fd %d on localhost:%d instead of 0x%x:%d\n", 81 | sockfd, newport, ntohl(addr_in->sin_addr.s_addr), oldport); 82 | 83 | /* Use a temporary location for the new address information */ 84 | addrlen_tmp = sizeof(addr_tmp); 85 | memcpy(&addr_tmp, addr, addrlen_tmp); 86 | 87 | /* Bind to other port on the loopback instead */ 88 | addr_tmp.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 89 | addr_tmp.sin_port = htons(newport); 90 | ret = (int) func(sockfd, &addr_tmp, addrlen_tmp); 91 | 92 | DEBUG("<< bind(%d, _, %d) ret %d\n", sockfd, addrlen, ret); 93 | return ret; 94 | } 95 | -------------------------------------------------------------------------------- /utils/launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage() { 4 | if [ "$*" ]; then 5 | echo "$*" 6 | echo 7 | fi 8 | echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT]" 9 | echo 10 | echo "Starts the WebSockets proxy and a mini-webserver and " 11 | echo "provides a cut-and-paste URL to go to." 12 | echo 13 | echo " --listen PORT Port for proxy/webserver to listen on" 14 | echo " Default: 6080" 15 | echo " --vnc VNC_HOST:PORT VNC server host:port proxy target" 16 | echo " Default: localhost:5900" 17 | echo " --cert CERT Path to combined cert/key file" 18 | echo " Default: self.pem" 19 | exit 2 20 | } 21 | 22 | NAME="$(basename $0)" 23 | HERE="$(cd "$(dirname "$0")" && pwd)" 24 | PORT="6080" 25 | VNC_DEST="localhost:5900" 26 | CERT="" 27 | proxy_pid="" 28 | 29 | die() { 30 | echo "$*" 31 | exit 1 32 | } 33 | 34 | cleanup() { 35 | trap - TERM QUIT INT EXIT 36 | trap "true" CHLD # Ignore cleanup messages 37 | echo 38 | if [ -n "${proxy_pid}" ]; then 39 | echo "Terminating WebSockets proxy (${proxy_pid})" 40 | kill ${proxy_pid} 41 | fi 42 | } 43 | 44 | # Process Arguments 45 | 46 | # Arguments that only apply to chrooter itself 47 | while [ "$*" ]; do 48 | param=$1; shift; OPTARG=$1 49 | case $param in 50 | --listen) PORT="${OPTARG}"; shift ;; 51 | --vnc) VNC_DEST="${OPTARG}"; shift ;; 52 | --cert) CERT="${OPTARG}"; shift ;; 53 | -h|--help) usage ;; 54 | -*) usage "Unknown chrooter option: ${param}" ;; 55 | *) break ;; 56 | esac 57 | done 58 | 59 | # Sanity checks 60 | which netstat >/dev/null 2>&1 \ 61 | || die "Must have netstat installed" 62 | 63 | netstat -ltn | grep -qs "${PORT}.*LISTEN" \ 64 | && die "Port ${PORT} in use. Try --listen PORT" 65 | 66 | trap "cleanup" TERM QUIT INT EXIT 67 | 68 | # Find vnc.html 69 | if [ -e "$(pwd)/vnc.html" ]; then 70 | WEB=$(pwd) 71 | elif [ -e "${HERE}/../vnc.html" ]; then 72 | WEB=${HERE}/../ 73 | elif [ -e "${HERE}/vnc.html" ]; then 74 | WEB=${HERE} 75 | else 76 | die "Could not find vnc.html" 77 | fi 78 | 79 | # Find self.pem 80 | if [ -n "${CERT}" ]; then 81 | if [ ! -e "${CERT}" ]; then 82 | die "Could not find ${CERT}" 83 | fi 84 | elif [ -e "$(pwd)/self.pem" ]; then 85 | CERT="$(pwd)/self.pem" 86 | elif [ -e "${HERE}/../self.pem" ]; then 87 | CERT="${HERE}/../self.pem" 88 | elif [ -e "${HERE}/self.pem" ]; then 89 | CERT="${HERE}/self.pem" 90 | else 91 | echo "Warning: could not find self.pem" 92 | fi 93 | 94 | echo "Starting webserver and WebSockets proxy on port ${PORT}" 95 | ${HERE}/wsproxy.py --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & 96 | proxy_pid="$!" 97 | sleep 1 98 | if ! ps -p ${proxy_pid} >/dev/null; then 99 | proxy_pid= 100 | echo "Failed to start WebSockets proxy" 101 | exit 1 102 | fi 103 | 104 | echo -e "\n\nNavigate to to this URL:\n" 105 | echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" 106 | echo -e "Press Ctrl-C to exit\n\n" 107 | 108 | wait ${proxy_pid} 109 | -------------------------------------------------------------------------------- /tests/base64.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Native Base64 Tests 5 | 6 | 7 | 8 | 9 | 10 |

Native Base64 Tests

11 | 12 |
13 | Messages:
14 | 15 | 16 |
17 | 18 | 19 | 92 | -------------------------------------------------------------------------------- /tests/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | Input Test 3 | 4 |

5 | 6 | Canvas:
7 | 9 | Canvas not supported. 10 | 11 | 12 |
13 | Results:
14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 100 | 101 | -------------------------------------------------------------------------------- /docs/rfb_notes: -------------------------------------------------------------------------------- 1 | 5.1.1 ProtocolVersion: 12, 12 bytes 2 | 3 | - Sent by server, max supported 4 | 12 ascii - "RFB 003.008\n" 5 | - Response by client, version to use 6 | 12 ascii - "RFB 003.003\n" 7 | 8 | 5.1.2 Authentication: >=4, [16, 4] bytes 9 | 10 | - Sent by server 11 | CARD32 - authentication-scheme 12 | 0 - connection failed 13 | CARD32 - length 14 | length - reason 15 | 1 - no authentication 16 | 17 | 2 - VNC authentication 18 | 16 CARD8 - challenge (random bytes) 19 | 20 | - Response by client (if VNC authentication) 21 | 16 CARD8 - client encrypts the challenge with DES, using user 22 | password as key, sends resulting 16 byte response 23 | 24 | - Response by server (if VNC authentication) 25 | CARD32 - 0 - OK 26 | 1 - failed 27 | 2 - too-many 28 | 29 | 5.1.3 ClientInitialisation: 1 byte 30 | - Sent by client 31 | CARD8 - shared-flag, 0 exclusive, non-zero shared 32 | 33 | 5.1.4 ServerInitialisation: >=24 bytes 34 | - Sent by server 35 | CARD16 - framebuffer-width 36 | CARD16 - framebuffer-height 37 | 16 byte PIXEL_FORMAT - server-pixel-format 38 | CARD8 - bits-per-pixel 39 | CARD8 - depth 40 | CARD8 - big-endian-flag, non-zero is big endian 41 | CARD8 - true-color-flag, non-zero then next 6 apply 42 | CARD16 - red-max 43 | CARD16 - green-max 44 | CARD16 - blue-max 45 | CARD8 - red-shift 46 | CARD8 - green-shift 47 | CARD8 - blue-shift 48 | 3 bytes - padding 49 | CARD32 - name-length 50 | 51 | CARD8[length] - name-string 52 | 53 | 54 | 55 | Client to Server Messages: 56 | 57 | 5.2.1 SetPixelFormat: 20 bytes 58 | CARD8: 0 - message-type 59 | ... 60 | 61 | 5.2.2 FixColourMapEntries: >=6 bytes 62 | CARD8: 1 - message-type 63 | ... 64 | 65 | 5.2.3 SetEncodings: >=8 bytes 66 | CARD8: 2 - message-type 67 | CARD8 - padding 68 | CARD16 - numer-of-encodings 69 | 70 | CARD32 - encoding-type in preference order 71 | 0 - raw 72 | 1 - copy-rectangle 73 | 2 - RRE 74 | 4 - CoRRE 75 | 5 - hextile 76 | 77 | 5.2.4 FramebufferUpdateRequest (10 bytes) 78 | CARD8: 3 - message-type 79 | CARD8 - incremental (0 for full-update, non-zero for incremental) 80 | CARD16 - x-position 81 | CARD16 - y-position 82 | CARD16 - width 83 | CARD16 - height 84 | 85 | 86 | 5.2.5 KeyEvent: 8 bytes 87 | CARD8: 4 - message-type 88 | CARD8 - down-flag 89 | 2 bytes - padding 90 | CARD32 - key (X-Windows keysym values) 91 | 92 | 5.2.6 PointerEvent: 6 bytes 93 | CARD8: 5 - message-type 94 | CARD8 - button-mask 95 | CARD16 - x-position 96 | CARD16 - y-position 97 | 98 | 5.2.7 ClientCutText: >=9 bytes 99 | CARD8: 6 - message-type 100 | ... 101 | 102 | 103 | Server to Client Messages: 104 | 105 | 5.3.1 FramebufferUpdate 106 | CARD8: 0 - message-type 107 | 1 byte - padding 108 | CARD16 - number-of-rectangles 109 | 110 | CARD16 - x-position 111 | CARD16 - y-position 112 | CARD16 - width 113 | CARD16 - height 114 | CARD16 - encoding-type: 115 | 0 - raw 116 | 1 - copy rectangle 117 | 2 - RRE 118 | 4 - CoRRE 119 | 5 - hextile 120 | 121 | raw: 122 | - width x height pixel values 123 | 124 | copy rectangle: 125 | CARD16 - src-x-position 126 | CARD16 - src-y-position 127 | 128 | RRE: 129 | CARD32 - N number-of-subrectangles 130 | Nxd bytes - background-pixel-value (d bits-per-pixel) 131 | 132 | ... 133 | 134 | 5.3.2 SetColourMapEntries (no support) 135 | CARD8: 1 - message-type 136 | ... 137 | 138 | 5.3.3 Bell 139 | CARD8: 2 - message-type 140 | 141 | 5.3.4 ServerCutText 142 | CARD8: 3 - message-type 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## noVNC: HTML5 VNC Client 2 | 3 | 4 | ### Description 5 | 6 | noVNC is a VNC client implemented using HTML5 technologies, 7 | specifically Canvas and WebSockets (supports 'wss://' encryption). 8 | noVNC is licensed under the 9 | [LGPLv3](http://www.gnu.org/licenses/lgpl.html). 10 | 11 | Special thanks to [Sentry Data Systems](http://www.sentryds.com) for 12 | sponsoring ongoing development of this project (and for employing me). 13 | 14 | Notable commits, announcements and news are posted to 15 | @noVNC 16 | 17 | 18 | ### Screenshots 19 | 20 | Running in Chrome before and after connecting: 21 | 22 |   23 | 24 | See more screenshots here. 25 | 26 | 27 | ### Projects/Companies using noVNC 28 | 29 | * [Sentry Data Systems](http://www.sentryds.com): uses noVNC in the 30 | [Datanex Cloud Computing Platform](http://www.sentryds.com/products/datanex/). 31 | 32 | * [Ganeti Web Manager](http://code.osuosl.org/projects/ganeti-webmgr): 33 | Feature [#1935](http://code.osuosl.org/issues/1935). 34 | 35 | * [Archipel](http://archipelproject.org): 36 | [Video demo](http://antoinemercadal.fr/archipelblog/wp-content/themes/ArchipelWPTemplate/video_youtube.php?title=VNC%20Demonstration&id=te_bzW574Zo) 37 | 38 | * [openQRM](http://www.openqrm.com/): VNC plugin available 39 | by request. Probably included in [version 40 | 4.8](http://www.openqrm.com/?q=node/15). [Video 41 | demo](http://www.openqrm-enterprise.com/news/details/article/remote-vm-console-plugin-available.html). 42 | 43 | * [OpenNode](http://www.opennodecloud.com/): uses noVNC in 44 | [OpenNode Management Console](http://opennode.activesys.org/about/software-included-in-opennode/). 45 | [OMC Screencast](http://opennode.activesys.org/about/opennode-management-console-screencast/). 46 | 47 | ### Browser Requirements 48 | 49 | * HTML5 Canvas: Except for Internet Explorer, most 50 | browsers have had Canvas support for quite some time. Internet 51 | Explorer 9 will have Canvas support (finally). 52 | 53 | * HTML5 WebSockets: For browsers that do not have builtin 54 | WebSockets support, the project includes 55 | web-socket-js, 56 | a WebSockets emulator using Adobe Flash. 57 | 58 | * Fast Javascript Engine: noVNC avoids using new Javascript 59 | functionality so it will run on older browsers, but decode and 60 | rendering happen in Javascript, so a slow Javascript engine will 61 | mean noVNC is painfully slow. 62 | 63 | * I maintain a more detailed list of browser compatibility here. 65 | 66 | 67 | ### Server Requirements 68 | 69 | Unless you are using a VNC server with support for WebSockets 70 | connections (only my [fork of libvncserver](http://github.com/kanaka/libvncserver) 71 | currently), you need to use a WebSockets to TCP socket proxy. There is 72 | a python proxy included ('websockify'). One advantage of using the 73 | proxy is that it has builtin support for SSL/TLS encryption (i.e. 74 | "wss://"). 75 | 76 | There a few reasons why a proxy is required: 77 | 78 | 1. WebSockets is not a pure socket protocol. There is an initial HTTP 79 | like handshake to allow easy hand-off by web servers and allow 80 | some origin policy exchange. Also, each WebSockets frame begins 81 | with 0 ('\x00') and ends with 255 ('\xff'). 82 | 83 | 2. Javascript itself does not have the ability to handle pure byte 84 | arrays. The python proxy encodes the data as base64 so that the 85 | Javascript client can decode the data as an integer array. 86 | 87 | 88 | ### Quick Start 89 | 90 | * Use the launch script to start a mini-webserver and the WebSockets 91 | proxy (websockify). The `--vnc` option is used to specify the location of 92 | a running VNC server: 93 | 94 | `./utils/launch.sh --vnc localhost:5901` 95 | 96 | * Point your browser to the cut-and-paste URL that is output by the 97 | launch script. Enter a password if the VNC server has one 98 | configured. Hit the Connect button and enjoy! 99 | 100 | 101 | ### Other Pages 102 | 103 | * [Advanced Usage](wiki/Advanced-usage). Generating an SSL 104 | certificate, starting a VNC server, advanced websockify usage, etc. 105 | 106 | * [Integrating noVNC](wiki/Integration) into existing projects. 107 | 108 | * [Troubleshooting noVNC](wiki/Troubleshooting) problems. 109 | 110 | 111 | -------------------------------------------------------------------------------- /include/webutil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * noVNC: HTML5 VNC client 3 | * Copyright (C) 2010 Joel Martin 4 | * Licensed under LGPL-3 (see LICENSE.txt) 5 | * 6 | * See README.md for usage and integration instructions. 7 | */ 8 | 9 | "use strict"; 10 | /*jslint bitwise: false, white: false */ 11 | /*global window, document */ 12 | 13 | // Globals defined here 14 | var WebUtil = {}, $D; 15 | 16 | /* 17 | * Simple DOM selector by ID 18 | */ 19 | if (!window.$D) { 20 | $D = function (id) { 21 | if (document.getElementById) { 22 | return document.getElementById(id); 23 | } else if (document.all) { 24 | return document.all[id]; 25 | } else if (document.layers) { 26 | return document.layers[id]; 27 | } 28 | return undefined; 29 | }; 30 | } 31 | 32 | 33 | /* 34 | * ------------------------------------------------------ 35 | * Namespaced in WebUtil 36 | * ------------------------------------------------------ 37 | */ 38 | 39 | // init log level reading the logging HTTP param 40 | WebUtil.init_logging = function() { 41 | Util._log_level = (document.location.href.match( 42 | /logging=([A-Za-z0-9\._\-]*)/) || 43 | ['', Util._log_level])[1]; 44 | 45 | Util.init_logging() 46 | } 47 | WebUtil.init_logging(); 48 | 49 | 50 | WebUtil.dirObj = function (obj, depth, parent) { 51 | var i, msg = "", val = ""; 52 | if (! depth) { depth=2; } 53 | if (! parent) { parent= ""; } 54 | 55 | // Print the properties of the passed-in object 56 | for (i in obj) { 57 | if ((depth > 1) && (typeof obj[i] === "object")) { 58 | // Recurse attributes that are objects 59 | msg += WebUtil.dirObj(obj[i], depth-1, parent + "." + i); 60 | } else { 61 | //val = new String(obj[i]).replace("\n", " "); 62 | val = obj[i].toString().replace("\n", " "); 63 | if (val.length > 30) { 64 | val = val.substr(0,30) + "..."; 65 | } 66 | msg += parent + "." + i + ": " + val + "\n"; 67 | } 68 | } 69 | return msg; 70 | }; 71 | 72 | // Read a query string variable 73 | WebUtil.getQueryVar = function(name, defVal) { 74 | var re = new RegExp('[?][^#]*' + name + '=([^&#]*)'); 75 | if (typeof defVal === 'undefined') { defVal = null; } 76 | return (document.location.href.match(re) || ['',defVal])[1]; 77 | }; 78 | 79 | 80 | /* 81 | * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html 82 | */ 83 | 84 | // No days means only for this browser session 85 | WebUtil.createCookie = function(name,value,days) { 86 | var date, expires; 87 | if (days) { 88 | date = new Date(); 89 | date.setTime(date.getTime()+(days*24*60*60*1000)); 90 | expires = "; expires="+date.toGMTString(); 91 | } 92 | else { 93 | expires = ""; 94 | } 95 | document.cookie = name+"="+value+expires+"; path=/"; 96 | }; 97 | 98 | WebUtil.readCookie = function(name, defaultValue) { 99 | var i, c, nameEQ = name + "=", ca = document.cookie.split(';'); 100 | for(i=0; i < ca.length; i += 1) { 101 | c = ca[i]; 102 | while (c.charAt(0) === ' ') { c = c.substring(1,c.length); } 103 | if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); } 104 | } 105 | return (typeof defaultValue !== 'undefined') ? defaultValue : null; 106 | }; 107 | 108 | WebUtil.eraseCookie = function(name) { 109 | WebUtil.createCookie(name,"",-1); 110 | }; 111 | 112 | /* 113 | * Alternate stylesheet selection 114 | */ 115 | WebUtil.getStylesheets = function() { var i, links, sheets = []; 116 | links = document.getElementsByTagName("link"); 117 | for (i = 0; i < links.length; i += 1) { 118 | if (links[i].title && 119 | links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { 120 | sheets.push(links[i]); 121 | } 122 | } 123 | return sheets; 124 | }; 125 | 126 | // No sheet means try and use value from cookie, null sheet used to 127 | // clear all alternates. 128 | WebUtil.selectStylesheet = function(sheet) { 129 | var i, link, sheets = WebUtil.getStylesheets(); 130 | if (typeof sheet === 'undefined') { 131 | sheet = 'default'; 132 | } 133 | for (i=0; i < sheets.length; i += 1) { 134 | link = sheets[i]; 135 | if (link.title === sheet) { 136 | Util.Debug("Using stylesheet " + sheet); 137 | link.disabled = false; 138 | } else { 139 | //Util.Debug("Skipping stylesheet " + link.title); 140 | link.disabled = true; 141 | } 142 | } 143 | return sheet; 144 | }; 145 | -------------------------------------------------------------------------------- /vnc_auto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | noVNC 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 31 |
Loading
28 | 30 |
32 |
33 | 34 | Canvas not supported. 35 | 36 |
37 | 38 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /tests/vnc_playback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | VNC Playback 4 | 5 | 6 | 7 | 8 | Iterations:   9 | Perftest:  10 | Realtime:   11 | 12 |   14 | 15 |

16 | 17 | Results:
18 | 19 | 20 |

21 | 22 |
23 |
24 | 25 | 26 |
Loading
27 |
28 | 29 | Canvas not supported. 30 | 31 |
32 | 33 | 34 | 35 | 39 | 40 | 43 | 44 | 45 | 46 | 131 | 132 | -------------------------------------------------------------------------------- /tests/cursor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cursor Change test 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Roll over the buttons to test cursors

17 |
18 | 19 | 20 | 21 |
22 |
23 |
24 | Debug:
25 | 26 |
27 |
28 | 29 | Canvas not supported. 30 | 31 | 32 | 33 | 34 | 136 | -------------------------------------------------------------------------------- /tests/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Canvas Performance Test 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Iterations:   16 | 17 | Width:   18 | Height:   19 | 20 |   22 | 23 |

24 | 25 | Canvas (should see three squares and two happy faces):
26 | 28 | Canvas not supported. 29 | 30 | 31 |
32 | Results:
33 | 34 | 35 | 36 | 147 | 148 | -------------------------------------------------------------------------------- /include/base64.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Modified from: 3 | * http://lxr.mozilla.org/mozilla/source/extensions/xml-rpc/src/nsXmlRpcClient.js#956 4 | */ 5 | 6 | /* ***** BEGIN LICENSE BLOCK ***** 7 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 8 | * 9 | * The contents of this file are subject to the Mozilla Public License Version 10 | * 1.1 (the "License"); you may not use this file except in compliance with 11 | * the License. You may obtain a copy of the License at 12 | * http://www.mozilla.org/MPL/ 13 | * 14 | * Software distributed under the License is distributed on an "AS IS" basis, 15 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 16 | * for the specific language governing rights and limitations under the 17 | * License. 18 | * 19 | * The Original Code is Mozilla XML-RPC Client component. 20 | * 21 | * The Initial Developer of the Original Code is 22 | * Digital Creations 2, Inc. 23 | * Portions created by the Initial Developer are Copyright (C) 2000 24 | * the Initial Developer. All Rights Reserved. 25 | * 26 | * Contributor(s): 27 | * Martijn Pieters (original author) 28 | * Samuel Sieb 29 | * 30 | * Alternatively, the contents of this file may be used under the terms of 31 | * either the GNU General Public License Version 2 or later (the "GPL"), or 32 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 33 | * in which case the provisions of the GPL or the LGPL are applicable instead 34 | * of those above. If you wish to allow use of your version of this file only 35 | * under the terms of either the GPL or the LGPL, and not to allow others to 36 | * use your version of this file under the terms of the MPL, indicate your 37 | * decision by deleting the provisions above and replace them with the notice 38 | * and other provisions required by the GPL or the LGPL. If you do not delete 39 | * the provisions above, a recipient may use your version of this file under 40 | * the terms of any one of the MPL, the GPL or the LGPL. 41 | * 42 | * ***** END LICENSE BLOCK ***** */ 43 | 44 | "use strict"; 45 | /*jslint white: false, bitwise: false, plusplus: false */ 46 | /*global console */ 47 | 48 | var Base64 = { 49 | 50 | /* Convert data (an array of integers) to a Base64 string. */ 51 | toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 52 | base64Pad : '=', 53 | 54 | encode: function (data) { 55 | var result = '', 56 | chrTable = Base64.toBase64Table.split(''), 57 | pad = Base64.base64Pad, 58 | length = data.length, 59 | i; 60 | // Convert every three bytes to 4 ascii characters. 61 | for (i = 0; i < (length - 2); i += 3) { 62 | result += chrTable[data[i] >> 2]; 63 | result += chrTable[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; 64 | result += chrTable[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; 65 | result += chrTable[data[i+2] & 0x3f]; 66 | } 67 | 68 | // Convert the remaining 1 or 2 bytes, pad out to 4 characters. 69 | if (length%3) { 70 | i = length - (length%3); 71 | result += chrTable[data[i] >> 2]; 72 | if ((length%3) === 2) { 73 | result += chrTable[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; 74 | result += chrTable[(data[i+1] & 0x0f) << 2]; 75 | result += pad; 76 | } else { 77 | result += chrTable[(data[i] & 0x03) << 4]; 78 | result += pad + pad; 79 | } 80 | } 81 | 82 | return result; 83 | }, 84 | 85 | /* Convert Base64 data to a string */ 86 | toBinaryTable : [ 87 | -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 88 | -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 89 | -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, 90 | 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, 91 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, 92 | 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, 93 | -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, 94 | 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 95 | ], 96 | 97 | decode: function (data, offset) { 98 | offset = typeof(offset) !== 'undefined' ? offset : 0; 99 | var binTable = Base64.toBinaryTable, 100 | pad = Base64.base64Pad, 101 | result, result_length, idx, i, c, padding, 102 | leftbits = 0, // number of bits decoded, but yet to be appended 103 | leftdata = 0, // bits decoded, but yet to be appended 104 | data_length = data.indexOf('=') - offset; 105 | 106 | if (data_length < 0) { data_length = data.length - offset; } 107 | 108 | /* Every four characters is 3 resulting numbers */ 109 | result_length = (data_length >> 2) * 3 + Math.floor((data_length%4)/1.5); 110 | result = new Array(result_length); 111 | 112 | // Convert one by one. 113 | for (idx = 0, i = offset; i < data.length; i++) { 114 | c = binTable[data.charCodeAt(i) & 0x7f]; 115 | padding = (data.charAt(i) === pad); 116 | // Skip illegal characters and whitespace 117 | if (c === -1) { 118 | console.error("Illegal character '" + data.charCodeAt(i) + "'"); 119 | continue; 120 | } 121 | 122 | // Collect data into leftdata, update bitcount 123 | leftdata = (leftdata << 6) | c; 124 | leftbits += 6; 125 | 126 | // If we have 8 or more bits, append 8 bits to the result 127 | if (leftbits >= 8) { 128 | leftbits -= 8; 129 | // Append if not padding. 130 | if (!padding) { 131 | result[idx++] = (leftdata >> leftbits) & 0xff; 132 | } 133 | leftdata &= (1 << leftbits) - 1; 134 | } 135 | } 136 | 137 | // If there are any bits left, the base64 string was corrupted 138 | if (leftbits) { 139 | throw {name: 'Base64-Error', 140 | message: 'Corrupted base64 string'}; 141 | } 142 | 143 | return result; 144 | } 145 | 146 | }; /* End of Base64 namespace */ 147 | -------------------------------------------------------------------------------- /include/web-socket-js/README.txt: -------------------------------------------------------------------------------- 1 | * How to try 2 | 3 | Assuming you have Web server (e.g. Apache) running at http://example.com/ . 4 | 5 | - Download web_socket.rb from: 6 | http://github.com/gimite/web-socket-ruby/tree/master 7 | - Run sample Web Socket server (echo server) in example.com with: (#1) 8 | $ ruby web-socket-ruby/samples/echo_server.rb example.com 10081 9 | - If your server already provides socket policy file at port 843, modify the file to allow access to port 10081. Otherwise you can skip this step. See below for details. 10 | - Publish the web-socket-js directory with your Web server (e.g. put it in ~/public_html). 11 | - Change ws://localhost:10081 to ws://example.com:10081 in sample.html. 12 | - Open sample.html in your browser. 13 | - After "onopen" is shown, input something, click [Send] and confirm echo back. 14 | 15 | #1: First argument of echo_server.rb means that it accepts Web Socket connection from HTML pages in example.com. 16 | 17 | 18 | * Troubleshooting 19 | 20 | If it doesn't work, try these: 21 | 22 | 1. Try Chrome and Firefox 3.x. 23 | - It doesn't work on Chrome: 24 | -- It's likely an issue of your code or the server. Debug your code as usual e.g. using console.log. 25 | - It works on Chrome but it doesn't work on Firefox: 26 | -- It's likely an issue of web-socket-js specific configuration (e.g. 3 and 4 below). 27 | - It works on both Chrome and Firefox, but it doesn't work on your browser: 28 | -- Check "Supported environment" section below. Your browser may not be supported by web-socket-js. 29 | 30 | 2. Add this line before your code: 31 | WEB_SOCKET_DEBUG = true; 32 | and use Developer Tools (Chrome/Safari) or Firebug (Firefox) to see if console.log outputs any errors. 33 | 34 | 3. Make sure you do NOT open your HTML page as local file e.g. file:///.../sample.html. web-socket-js doesn't work on local file. Open it via Web server e.g. http:///.../sample.html. 35 | 36 | 4. If you are NOT using web-socket-ruby as your WebSocket server, you need to place Flash socket policy file on your server. See "Flash socket policy file" section below for details. 37 | 38 | 5. Check if sample.html bundled with web-socket-js works. 39 | 40 | 6. Make sure the port used for WebSocket (10081 in example above) is not blocked by your server/client's firewall. 41 | 42 | 7. Install debugger version of Flash Player available here to see Flash errors: 43 | http://www.adobe.com/support/flashplayer/downloads.html 44 | 45 | 46 | * Supported environments 47 | 48 | It should work on: 49 | - Google Chrome 4 or later (just uses native implementation) 50 | - Firefox 3.x, Internet Explorer 8 + Flash Player 9 or later 51 | 52 | It may or may not work on other browsers such as Safari, Opera or IE 6. Patch for these browsers are appreciated, but I will not work on fixing issues specific to these browsers by myself. 53 | 54 | 55 | * Flash socket policy file 56 | 57 | This implementation uses Flash's socket, which means that your server must provide Flash socket policy file to declare the server accepts connections from Flash. 58 | 59 | If you use web-socket-ruby available at 60 | http://github.com/gimite/web-socket-ruby/tree/master 61 | , you don't need anything special, because web-socket-ruby handles Flash socket policy file request. But if you already provide socket policy file at port 843, you need to modify the file to allow access to Web Socket port, because it precedes what web-socket-ruby provides. 62 | 63 | If you use other Web Socket server implementation, you need to provide socket policy file yourself. See 64 | http://www.lightsphere.com/dev/articles/flash_socket_policy.html 65 | for details and sample script to run socket policy file server. node.js implementation is available here: 66 | http://github.com/LearnBoost/Socket.IO-node/blob/master/lib/socket.io/transports/flashsocket.js 67 | 68 | Actually, it's still better to provide socket policy file at port 843 even if you use web-socket-ruby. Flash always try to connect to port 843 first, so providing the file at port 843 makes startup faster. 69 | 70 | 71 | * Cookie considerations 72 | 73 | Cookie is sent if Web Socket host is the same as the origin of JavaScript. Otherwise it is not sent, because I don't know way to send right Cookie (which is Cookie of the host of Web Socket, I heard). 74 | 75 | Note that it's technically possible that client sends arbitrary string as Cookie and any other headers (by modifying this library for example) once you place Flash socket policy file in your server. So don't trust Cookie and other headers if you allow connection from untrusted origin. 76 | 77 | 78 | * Proxy considerations 79 | 80 | The WebSocket spec (http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol) specifies instructions for User Agents to support proxied connections by implementing the HTTP CONNECT method. 81 | 82 | The AS3 Socket class doesn't implement this mechanism, which renders it useless for the scenarios where the user trying to open a socket is behind a proxy. 83 | 84 | The class RFC2817Socket (by Christian Cantrell) effectively lets us implement this, as long as the proxy settings are known and provided by the interface that instantiates the WebSocket. As such, if you want to support proxied conncetions, you'll have to supply this information to the WebSocket constructor when Flash is being used. One way to go about it would be to ask the user for proxy settings information if the initial connection fails. 85 | 86 | 87 | * How to host HTML file and SWF file in different domains 88 | 89 | By default, HTML file and SWF file must be in the same domain. You can follow steps below to allow hosting them in different domain. 90 | 91 | WARNING: If you use the method below, HTML files in ANY domains can send arbitrary TCP data to your WebSocket server, regardless of configuration in Flash socket policy file. Arbitrary TCP data means that they can even fake request headers including Origin and Cookie. 92 | 93 | - Unzip WebSocketMainInsecure.zip to extract WebSocketMainInsecure.swf. 94 | - Put WebSocketMainInsecure.swf on your server, instead of WebSocketMain.swf. 95 | - In JavaScript, set WEB_SOCKET_SWF_LOCATION to URL of your WebSocketMainInsecure.swf. 96 | 97 | 98 | * How to build WebSocketMain.swf 99 | 100 | Install Flex 4 SDK: 101 | http://opensource.adobe.com/wiki/display/flexsdk/Download+Flex+4 102 | 103 | $ cd flash-src 104 | $ ./build.sh 105 | 106 | 107 | * License 108 | 109 | New BSD License. 110 | -------------------------------------------------------------------------------- /tests/vnc_perf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | VNC Performance Benchmark 4 | 5 | 6 | 7 | 8 | Passes:   9 | 10 |   12 | 13 |

14 | 15 | Results:
16 | 17 | 18 |

19 | 20 |
21 |
22 | 23 | 24 |
Loading
25 |
26 | 27 | Canvas not supported. 28 | 29 |
30 | 31 | 32 | 33 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 200 | 201 | -------------------------------------------------------------------------------- /tests/json2graph.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Use matplotlib to generate performance charts 5 | Copyright 2011 Joel Martin 6 | Licensed under GPL version 3 (see docs/LICENSE.GPL-3) 7 | ''' 8 | 9 | # a bar plot with errorbars 10 | import sys, json, pprint 11 | import numpy as np 12 | import matplotlib.pyplot as plt 13 | from matplotlib.font_manager import FontProperties 14 | 15 | def usage(): 16 | print "%s json_file level1 level2 level3\n\n" % sys.argv[0] 17 | print "Description:\n" 18 | print "level1, level2, and level3 are one each of the following:\n"; 19 | print " select=ITEM - select only ITEM at this level"; 20 | print " bar - each item on this level becomes a graph bar"; 21 | print " group - items on this level become groups of bars"; 22 | print "\n"; 23 | print "json_file is a file containing json data in the following format:\n" 24 | print ' {'; 25 | print ' "conf": {'; 26 | print ' "order_l1": ['; 27 | print ' "level1_label1",'; 28 | print ' "level1_label2",'; 29 | print ' ...'; 30 | print ' ],'; 31 | print ' "order_l2": ['; 32 | print ' "level2_label1",'; 33 | print ' "level2_label2",'; 34 | print ' ...'; 35 | print ' ],'; 36 | print ' "order_l3": ['; 37 | print ' "level3_label1",'; 38 | print ' "level3_label2",'; 39 | print ' ...'; 40 | print ' ]'; 41 | print ' },'; 42 | print ' "stats": {'; 43 | print ' "level1_label1": {'; 44 | print ' "level2_label1": {'; 45 | print ' "level3_label1": [val1, val2, val3],'; 46 | print ' "level3_label2": [val1, val2, val3],'; 47 | print ' ...'; 48 | print ' },'; 49 | print ' "level2_label2": {'; 50 | print ' ...'; 51 | print ' },'; 52 | print ' },'; 53 | print ' "level1_label2": {'; 54 | print ' ...'; 55 | print ' },'; 56 | print ' ...'; 57 | print ' },'; 58 | print ' }'; 59 | sys.exit(2) 60 | 61 | def error(msg): 62 | print msg 63 | sys.exit(1) 64 | 65 | 66 | #colors = ['#ff0000', '#0863e9', '#00f200', '#ffa100', 67 | # '#800000', '#805100', '#013075', '#007900'] 68 | colors = ['#ff0000', '#00ff00', '#0000ff', 69 | '#dddd00', '#dd00dd', '#00dddd', 70 | '#dd6622', '#dd2266', '#66dd22', 71 | '#8844dd', '#44dd88', '#4488dd'] 72 | 73 | if len(sys.argv) < 5: 74 | usage() 75 | 76 | filename = sys.argv[1] 77 | L1 = sys.argv[2] 78 | L2 = sys.argv[3] 79 | L3 = sys.argv[4] 80 | if len(sys.argv) > 5: 81 | legendHeight = float(sys.argv[5]) 82 | else: 83 | legendHeight = 0.75 84 | 85 | # Load the JSON data from the file 86 | data = json.loads(file(filename).read()) 87 | conf = data['conf'] 88 | stats = data['stats'] 89 | 90 | # Sanity check data hierarchy 91 | if len(conf['order_l1']) != len(stats.keys()): 92 | error("conf.order_l1 does not match stats level 1") 93 | for l1 in stats.keys(): 94 | if len(conf['order_l2']) != len(stats[l1].keys()): 95 | error("conf.order_l2 does not match stats level 2 for %s" % l1) 96 | if conf['order_l1'].count(l1) < 1: 97 | error("%s not found in conf.order_l1" % l1) 98 | for l2 in stats[l1].keys(): 99 | if len(conf['order_l3']) != len(stats[l1][l2].keys()): 100 | error("conf.order_l3 does not match stats level 3") 101 | if conf['order_l2'].count(l2) < 1: 102 | error("%s not found in conf.order_l2" % l2) 103 | for l3 in stats[l1][l2].keys(): 104 | if conf['order_l3'].count(l3) < 1: 105 | error("%s not found in conf.order_l3" % l3) 106 | 107 | # 108 | # Generate the data based on the level specifications 109 | # 110 | bar_labels = None 111 | group_labels = None 112 | bar_vals = [] 113 | bar_sdvs = [] 114 | if L3.startswith("select="): 115 | select_label = l3 = L3.split("=")[1] 116 | bar_labels = conf['order_l1'] 117 | group_labels = conf['order_l2'] 118 | bar_vals = [[0]*len(group_labels) for i in bar_labels] 119 | bar_sdvs = [[0]*len(group_labels) for i in bar_labels] 120 | for b in range(len(bar_labels)): 121 | l1 = bar_labels[b] 122 | for g in range(len(group_labels)): 123 | l2 = group_labels[g] 124 | bar_vals[b][g] = np.mean(stats[l1][l2][l3]) 125 | bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) 126 | elif L2.startswith("select="): 127 | select_label = l2 = L2.split("=")[1] 128 | bar_labels = conf['order_l1'] 129 | group_labels = conf['order_l3'] 130 | bar_vals = [[0]*len(group_labels) for i in bar_labels] 131 | bar_sdvs = [[0]*len(group_labels) for i in bar_labels] 132 | for b in range(len(bar_labels)): 133 | l1 = bar_labels[b] 134 | for g in range(len(group_labels)): 135 | l3 = group_labels[g] 136 | bar_vals[b][g] = np.mean(stats[l1][l2][l3]) 137 | bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) 138 | elif L1.startswith("select="): 139 | select_label = l1 = L1.split("=")[1] 140 | bar_labels = conf['order_l2'] 141 | group_labels = conf['order_l3'] 142 | bar_vals = [[0]*len(group_labels) for i in bar_labels] 143 | bar_sdvs = [[0]*len(group_labels) for i in bar_labels] 144 | for b in range(len(bar_labels)): 145 | l2 = bar_labels[b] 146 | for g in range(len(group_labels)): 147 | l3 = group_labels[g] 148 | bar_vals[b][g] = np.mean(stats[l1][l2][l3]) 149 | bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) 150 | else: 151 | usage() 152 | 153 | # If group is before bar then flip (zip) the data 154 | if [L1, L2, L3].index("group") < [L1, L2, L3].index("bar"): 155 | bar_labels, group_labels = group_labels, bar_labels 156 | bar_vals = zip(*bar_vals) 157 | bar_sdvs = zip(*bar_sdvs) 158 | 159 | print "bar_vals:", bar_vals 160 | 161 | # 162 | # Now render the bar graph 163 | # 164 | ind = np.arange(len(group_labels)) # the x locations for the groups 165 | width = 0.8 * (1.0/len(bar_labels)) # the width of the bars 166 | 167 | fig = plt.figure(figsize=(10,6), dpi=80) 168 | plot = fig.add_subplot(1, 1, 1) 169 | 170 | rects = [] 171 | for i in range(len(bar_vals)): 172 | rects.append(plot.bar(ind+width*i, bar_vals[i], width, color=colors[i], 173 | yerr=bar_sdvs[i], align='center')) 174 | 175 | # add some 176 | plot.set_ylabel('Milliseconds (less is better)') 177 | plot.set_title("Javascript array test: %s" % select_label) 178 | plot.set_xticks(ind+width) 179 | plot.set_xticklabels( group_labels ) 180 | 181 | fontP = FontProperties() 182 | fontP.set_size('small') 183 | plot.legend( [r[0] for r in rects], bar_labels, prop=fontP, 184 | loc = 'center right', bbox_to_anchor = (1.0, legendHeight)) 185 | 186 | def autolabel(rects): 187 | # attach some text labels 188 | for rect in rects: 189 | height = rect.get_height() 190 | if np.isnan(height): 191 | height = 0.0 192 | plot.text(rect.get_x()+rect.get_width()/2., height+20, '%d'%int(height), 193 | ha='center', va='bottom', size='7') 194 | 195 | for rect in rects: 196 | autolabel(rect) 197 | 198 | # Adjust axis sizes 199 | axis = list(plot.axis()) 200 | axis[0] = -width # Make sure left side has enough for bar 201 | #axis[1] = axis[1] * 1.20 # Add 20% to the right to make sure it fits 202 | axis[2] = 0 # Make y-axis start at 0 203 | axis[3] = axis[3] * 1.10 # Add 10% to the top 204 | plot.axis(axis) 205 | 206 | plt.show() 207 | -------------------------------------------------------------------------------- /include/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * noVNC: HTML5 VNC client 3 | * Copyright (C) 2010 Joel Martin 4 | * Licensed under LGPL-3 (see LICENSE.txt) 5 | * 6 | * See README.md for usage and integration instructions. 7 | */ 8 | 9 | "use strict"; 10 | /*jslint bitwise: false, white: false */ 11 | /*global window, console, document, navigator, ActiveXObject */ 12 | 13 | // Globals defined here 14 | var Util = {}; 15 | 16 | 17 | /* 18 | * Make arrays quack 19 | */ 20 | 21 | Array.prototype.push8 = function (num) { 22 | this.push(num & 0xFF); 23 | }; 24 | 25 | Array.prototype.push16 = function (num) { 26 | this.push((num >> 8) & 0xFF, 27 | (num ) & 0xFF ); 28 | }; 29 | Array.prototype.push32 = function (num) { 30 | this.push((num >> 24) & 0xFF, 31 | (num >> 16) & 0xFF, 32 | (num >> 8) & 0xFF, 33 | (num ) & 0xFF ); 34 | }; 35 | 36 | /* 37 | * ------------------------------------------------------ 38 | * Namespaced in Util 39 | * ------------------------------------------------------ 40 | */ 41 | 42 | /* 43 | * Logging/debug routines 44 | */ 45 | 46 | Util._log_level = 'warn'; 47 | Util.init_logging = function (level) { 48 | if (typeof level === 'undefined') { 49 | level = Util._log_level; 50 | } else { 51 | Util._log_level = level; 52 | } 53 | if (typeof window.console === "undefined") { 54 | if (typeof window.opera !== "undefined") { 55 | window.console = { 56 | 'log' : window.opera.postError, 57 | 'warn' : window.opera.postError, 58 | 'error': window.opera.postError }; 59 | } else { 60 | window.console = { 61 | 'log' : function(m) {}, 62 | 'warn' : function(m) {}, 63 | 'error': function(m) {}}; 64 | } 65 | } 66 | 67 | Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; 68 | switch (level) { 69 | case 'debug': Util.Debug = function (msg) { console.log(msg); }; 70 | case 'info': Util.Info = function (msg) { console.log(msg); }; 71 | case 'warn': Util.Warn = function (msg) { console.warn(msg); }; 72 | case 'error': Util.Error = function (msg) { console.error(msg); }; 73 | case 'none': 74 | break; 75 | default: 76 | throw("invalid logging type '" + level + "'"); 77 | } 78 | }; 79 | Util.get_logging = function () { 80 | return Util._log_level; 81 | } 82 | // Initialize logging level 83 | Util.init_logging(); 84 | 85 | 86 | // Set defaults for Crockford style function namespaces 87 | Util.conf_default = function(cfg, api, v, type, defval, desc) { 88 | // Description 89 | api['get_' + v + '_desc'] = desc; 90 | // Default getter 91 | if (typeof api['get_' + v] === 'undefined') { 92 | api['get_' + v] = function () { 93 | return cfg[v]; 94 | }; 95 | } 96 | // Default setter 97 | if (typeof api['set_' + v] === 'undefined') { 98 | api['set_' + v] = function (val) { 99 | if (type in {'boolean':1, 'bool':1}) { 100 | if ((!val) || (val in {'0':1, 'no':1, 'false':1})) { 101 | val = false; 102 | } else { 103 | val = true; 104 | } 105 | } else if (type in {'integer':1, 'int':1}) { 106 | val = parseInt(val, 10); 107 | } 108 | cfg[v] = val; 109 | }; 110 | } 111 | 112 | if (typeof cfg[v] === 'undefined') { 113 | // Set to default 114 | api['set_' + v](defval); 115 | } else { 116 | // Coerce existing setting to the right type 117 | api['set_' + v](cfg[v]); 118 | } 119 | }; 120 | 121 | 122 | 123 | /* 124 | * Cross-browser routines 125 | */ 126 | 127 | // Get DOM element position on page 128 | Util.getPosition = function (obj) { 129 | var x = 0, y = 0; 130 | if (obj.offsetParent) { 131 | do { 132 | x += obj.offsetLeft; 133 | y += obj.offsetTop; 134 | obj = obj.offsetParent; 135 | } while (obj); 136 | } 137 | return {'x': x, 'y': y}; 138 | }; 139 | 140 | // Get mouse event position in DOM element 141 | Util.getEventPosition = function (e, obj, scale) { 142 | var evt, docX, docY, pos; 143 | //if (!e) evt = window.event; 144 | evt = (e ? e : window.event); 145 | if (evt.pageX || evt.pageY) { 146 | docX = evt.pageX; 147 | docY = evt.pageY; 148 | } else if (evt.clientX || evt.clientY) { 149 | docX = evt.clientX + document.body.scrollLeft + 150 | document.documentElement.scrollLeft; 151 | docY = evt.clientY + document.body.scrollTop + 152 | document.documentElement.scrollTop; 153 | } 154 | pos = Util.getPosition(obj); 155 | if (typeof scale === "undefined") { 156 | scale = 1; 157 | } 158 | return {'x': (docX - pos.x) / scale, 'y': (docY - pos.y) / scale}; 159 | }; 160 | 161 | 162 | // Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events 163 | Util.addEvent = function (obj, evType, fn){ 164 | if (obj.attachEvent){ 165 | var r = obj.attachEvent("on"+evType, fn); 166 | return r; 167 | } else if (obj.addEventListener){ 168 | obj.addEventListener(evType, fn, false); 169 | return true; 170 | } else { 171 | throw("Handler could not be attached"); 172 | } 173 | }; 174 | 175 | Util.removeEvent = function(obj, evType, fn){ 176 | if (obj.detachEvent){ 177 | var r = obj.detachEvent("on"+evType, fn); 178 | return r; 179 | } else if (obj.removeEventListener){ 180 | obj.removeEventListener(evType, fn, false); 181 | return true; 182 | } else { 183 | throw("Handler could not be removed"); 184 | } 185 | }; 186 | 187 | Util.stopEvent = function(e) { 188 | if (e.stopPropagation) { e.stopPropagation(); } 189 | else { e.cancelBubble = true; } 190 | 191 | if (e.preventDefault) { e.preventDefault(); } 192 | else { e.returnValue = false; } 193 | }; 194 | 195 | 196 | // Set browser engine versions. Based on mootools. 197 | Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; 198 | 199 | Util.Engine = { 200 | 'presto': (function() { 201 | return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), 202 | 'trident': (function() { 203 | return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()), 204 | 'webkit': (function() { 205 | try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), 206 | //'webkit': (function() { 207 | // return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()), 208 | 'gecko': (function() { 209 | return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }()) 210 | }; 211 | if (Util.Engine.webkit) { 212 | // Extract actual webkit version if available 213 | Util.Engine.webkit = (function(v) { 214 | var re = new RegExp('WebKit/([0-9\.]*) '); 215 | v = (navigator.userAgent.match(re) || ['', v])[1]; 216 | return parseFloat(v, 10); 217 | })(Util.Engine.webkit); 218 | } 219 | 220 | Util.Flash = (function(){ 221 | var v, version; 222 | try { 223 | v = navigator.plugins['Shockwave Flash'].description; 224 | } catch(err1) { 225 | try { 226 | v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); 227 | } catch(err2) { 228 | v = '0 r0'; 229 | } 230 | } 231 | version = v.match(/\d+/g); 232 | return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; 233 | }()); 234 | -------------------------------------------------------------------------------- /docs/LICENSE.LGPL-3: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /include/websock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Websock: high-performance binary WebSockets 3 | * Copyright (C) 2011 Joel Martin 4 | * Licensed under LGPL-3 (see LICENSE.txt) 5 | * 6 | * Websock is similar to the standard WebSocket object but Websock 7 | * enables communication with raw TCP sockets (i.e. the binary stream) 8 | * via websockify. This is accomplished by base64 encoding the data 9 | * stream between Websock and websockify. 10 | * 11 | * Websock has built-in receive queue buffering; the message event 12 | * does not contain actual data but is simply a notification that 13 | * there is new data available. Several rQ* methods are available to 14 | * read binary data off of the receive queue. 15 | */ 16 | 17 | 18 | // Load Flash WebSocket emulator if needed 19 | 20 | if (window.WebSocket) { 21 | Websock_native = true; 22 | } else { 23 | /* no builtin WebSocket so load web_socket.js */ 24 | Websock_native = false; 25 | (function () { 26 | function get_INCLUDE_URI() { 27 | return (typeof INCLUDE_URI !== "undefined") ? 28 | INCLUDE_URI : "include/"; 29 | } 30 | 31 | var start = "