From 8d8bd03cbe4a5c825e68383a22bcfaa284bb7b29 Mon Sep 17 00:00:00 2001 From: User Web Date: Mon, 19 Nov 2018 14:09:09 +0300 Subject: [PATCH] graph wip --- .gitignore | 5 +- media_import/placeholder | 2 + php/clonos.php | 4 +- public/css/styles.css | 43 +- public/dialogs/vnc-bhyve.php | 8 + public/js/clonos.js | 236 +- public/novnc/CONTRIBUTING.md | 54 + public/novnc/LICENSE.txt | 84 + public/novnc/README.md | 132 + public/novnc/docs/LICENSE.Apache-2.0 | 202 ++ public/novnc/docs/LICENSE.BSD-2-Clause | 22 + public/novnc/docs/LICENSE.BSD-3-Clause | 24 + public/novnc/docs/LICENSE.GPL-3 | 621 +++++ public/novnc/docs/LICENSE.LGPL-3 | 165 ++ public/novnc/docs/LICENSE.MPL-2.0 | 373 +++ public/novnc/docs/LICENSE.OFL-1.1 | 91 + public/novnc/docs/LICENSE.pako | 21 + public/novnc/docs/LICENSE.zlib | 27 + public/novnc/docs/VERSION | 1 + public/novnc/docs/flash_policy.txt | 4 + public/novnc/docs/links | 76 + public/novnc/docs/notes | 5 + public/novnc/docs/release.txt | 34 + public/novnc/docs/rfb_notes | 147 + public/novnc/docs/rfbproto-3.3.pdf | Bin 0 -> 110778 bytes public/novnc/docs/rfbproto-3.7.pdf | Bin 0 -> 165552 bytes public/novnc/docs/rfbproto-3.8.pdf | Bin 0 -> 143840 bytes public/novnc/favicon.ico | 1 + public/novnc/images/alt.png | Bin 0 -> 339 bytes public/novnc/images/clipboard.png | Bin 0 -> 501 bytes public/novnc/images/connect.png | Bin 0 -> 404 bytes public/novnc/images/ctrl.png | Bin 0 -> 354 bytes public/novnc/images/ctrlaltdel.png | Bin 0 -> 317 bytes public/novnc/images/disconnect.png | Bin 0 -> 1378 bytes public/novnc/images/drag.png | Bin 0 -> 963 bytes public/novnc/images/esc.png | Bin 0 -> 385 bytes public/novnc/images/favicon.ico | Bin 0 -> 1150 bytes public/novnc/images/favicon.png | Bin 0 -> 453 bytes public/novnc/images/fullscreen.png | Bin 0 -> 851 bytes public/novnc/images/keyboard.png | Bin 0 -> 1283 bytes public/novnc/images/mouse_left.png | Bin 0 -> 511 bytes public/novnc/images/mouse_middle.png | Bin 0 -> 517 bytes public/novnc/images/mouse_none.png | Bin 0 -> 497 bytes public/novnc/images/mouse_right.png | Bin 0 -> 513 bytes public/novnc/images/power.png | Bin 0 -> 390 bytes public/novnc/images/screen_320x460.png | Bin 0 -> 12778 bytes public/novnc/images/screen_57x57.png | Bin 0 -> 1807 bytes public/novnc/images/screen_700x700.png | Bin 0 -> 17930 bytes public/novnc/images/settings.png | Bin 0 -> 2495 bytes public/novnc/images/tab.png | Bin 0 -> 387 bytes public/novnc/images/toggleextrakeys.png | Bin 0 -> 735 bytes public/novnc/include/Orbitron700.ttf | Bin 0 -> 38580 bytes public/novnc/include/Orbitron700.woff | Bin 0 -> 17472 bytes public/novnc/include/base.css | 527 ++++ public/novnc/include/base64.js | 113 + public/novnc/include/black.css | 71 + public/novnc/include/blue.css | 64 + public/novnc/include/chrome-app/tcp-client.js | 321 +++ public/novnc/include/des.js | 276 ++ public/novnc/include/display.js | 908 +++++++ public/novnc/include/inflator.js | 2418 +++++++++++++++++ public/novnc/include/input.js | 389 +++ public/novnc/include/keyboard.js | 543 ++++ public/novnc/include/keysym.js | 378 +++ public/novnc/include/keysymdef.js | 15 + public/novnc/include/logo.js | 1 + public/novnc/include/playback.js | 120 + public/novnc/include/rfb.js | 2148 +++++++++++++++ public/novnc/include/ui.js | 1287 +++++++++ public/novnc/include/util.js | 622 +++++ public/novnc/include/websock.js | 440 +++ public/novnc/include/webutil.js | 292 ++ public/novnc/karma.conf.js | 197 ++ public/novnc/package.json | 50 + public/novnc/tests/arrays.html | 39 + public/novnc/tests/arrays.js | 375 +++ public/novnc/tests/assertions.js | 98 + public/novnc/tests/base64.html | 91 + public/novnc/tests/base64.js | 12 + public/novnc/tests/browser.js | 134 + public/novnc/tests/canvas.html | 148 + public/novnc/tests/cursor.html | 135 + public/novnc/tests/face.png | Bin 0 -> 2303 bytes public/novnc/tests/face.png.js | 1 + public/novnc/tests/fake.websocket.js | 91 + public/novnc/tests/input.html | 132 + public/novnc/tests/keyboard-tests.html | 29 + public/novnc/tests/run_from_console.casper.js | 114 + public/novnc/tests/run_from_console.js | 361 +++ public/novnc/tests/run_from_console.zombie.js | 82 + public/novnc/tests/stats.js | 53 + public/novnc/tests/test.base64.js | 33 + public/novnc/tests/test.display.js | 461 ++++ public/novnc/tests/test.helper.js | 262 ++ public/novnc/tests/test.keyboard.js | 842 ++++++ public/novnc/tests/test.rfb.js | 1929 +++++++++++++ public/novnc/tests/test.util.js | 105 + public/novnc/tests/test.websock.js | 457 ++++ public/novnc/tests/viewport.css | 43 + public/novnc/tests/viewport.html | 203 ++ public/novnc/tests/vnc_perf.html | 213 ++ public/novnc/tests/vnc_playback.html | 137 + public/novnc/utils/README.md | 14 + public/novnc/utils/b64-to-binary.pl | 17 + public/novnc/utils/img2js.py | 40 + public/novnc/utils/inflator.partial.js | 40 + public/novnc/utils/json2graph.py | 206 ++ public/novnc/utils/launch.sh | 154 ++ public/novnc/utils/parse.js | 97 + public/novnc/utils/u2x11 | 28 + public/novnc/utils/websockify | 1 + public/novnc/vnc.html | 225 ++ public/novnc/vnc_auto.html | 247 ++ public/pages/bhyvevms/bhyveslist.table | 2 +- public/pages/bhyvevms/en.index.php | 1 + public/pages/jailscontainers/en.index.php | 1 + public/pages/jailscontainers/jailslist.table | 2 +- public/pages/overview/ru.index.php | 52 +- 118 files changed, 21197 insertions(+), 72 deletions(-) create mode 100644 media_import/placeholder create mode 100644 public/novnc/CONTRIBUTING.md create mode 100644 public/novnc/LICENSE.txt create mode 100644 public/novnc/README.md create mode 100644 public/novnc/docs/LICENSE.Apache-2.0 create mode 100644 public/novnc/docs/LICENSE.BSD-2-Clause create mode 100644 public/novnc/docs/LICENSE.BSD-3-Clause create mode 100644 public/novnc/docs/LICENSE.GPL-3 create mode 100644 public/novnc/docs/LICENSE.LGPL-3 create mode 100644 public/novnc/docs/LICENSE.MPL-2.0 create mode 100644 public/novnc/docs/LICENSE.OFL-1.1 create mode 100644 public/novnc/docs/LICENSE.pako create mode 100644 public/novnc/docs/LICENSE.zlib create mode 100644 public/novnc/docs/VERSION create mode 100644 public/novnc/docs/flash_policy.txt create mode 100644 public/novnc/docs/links create mode 100644 public/novnc/docs/notes create mode 100644 public/novnc/docs/release.txt create mode 100644 public/novnc/docs/rfb_notes create mode 100644 public/novnc/docs/rfbproto-3.3.pdf create mode 100644 public/novnc/docs/rfbproto-3.7.pdf create mode 100644 public/novnc/docs/rfbproto-3.8.pdf create mode 120000 public/novnc/favicon.ico create mode 100644 public/novnc/images/alt.png create mode 100644 public/novnc/images/clipboard.png create mode 100644 public/novnc/images/connect.png create mode 100644 public/novnc/images/ctrl.png create mode 100644 public/novnc/images/ctrlaltdel.png create mode 100644 public/novnc/images/disconnect.png create mode 100644 public/novnc/images/drag.png create mode 100644 public/novnc/images/esc.png create mode 100644 public/novnc/images/favicon.ico create mode 100644 public/novnc/images/favicon.png create mode 100644 public/novnc/images/fullscreen.png create mode 100644 public/novnc/images/keyboard.png create mode 100644 public/novnc/images/mouse_left.png create mode 100644 public/novnc/images/mouse_middle.png create mode 100644 public/novnc/images/mouse_none.png create mode 100644 public/novnc/images/mouse_right.png create mode 100644 public/novnc/images/power.png create mode 100644 public/novnc/images/screen_320x460.png create mode 100644 public/novnc/images/screen_57x57.png create mode 100644 public/novnc/images/screen_700x700.png create mode 100644 public/novnc/images/settings.png create mode 100644 public/novnc/images/tab.png create mode 100644 public/novnc/images/toggleextrakeys.png create mode 100644 public/novnc/include/Orbitron700.ttf create mode 100644 public/novnc/include/Orbitron700.woff create mode 100644 public/novnc/include/base.css create mode 100644 public/novnc/include/base64.js create mode 100644 public/novnc/include/black.css create mode 100644 public/novnc/include/blue.css create mode 100644 public/novnc/include/chrome-app/tcp-client.js create mode 100644 public/novnc/include/des.js create mode 100644 public/novnc/include/display.js create mode 100644 public/novnc/include/inflator.js create mode 100644 public/novnc/include/input.js create mode 100644 public/novnc/include/keyboard.js create mode 100644 public/novnc/include/keysym.js create mode 100644 public/novnc/include/keysymdef.js create mode 100644 public/novnc/include/logo.js create mode 100644 public/novnc/include/playback.js create mode 100644 public/novnc/include/rfb.js create mode 100644 public/novnc/include/ui.js create mode 100644 public/novnc/include/util.js create mode 100644 public/novnc/include/websock.js create mode 100644 public/novnc/include/webutil.js create mode 100644 public/novnc/karma.conf.js create mode 100644 public/novnc/package.json create mode 100644 public/novnc/tests/arrays.html create mode 100644 public/novnc/tests/arrays.js create mode 100644 public/novnc/tests/assertions.js create mode 100644 public/novnc/tests/base64.html create mode 100644 public/novnc/tests/base64.js create mode 100644 public/novnc/tests/browser.js create mode 100644 public/novnc/tests/canvas.html create mode 100644 public/novnc/tests/cursor.html create mode 100644 public/novnc/tests/face.png create mode 100644 public/novnc/tests/face.png.js create mode 100644 public/novnc/tests/fake.websocket.js create mode 100644 public/novnc/tests/input.html create mode 100644 public/novnc/tests/keyboard-tests.html create mode 100644 public/novnc/tests/run_from_console.casper.js create mode 100644 public/novnc/tests/run_from_console.js create mode 100644 public/novnc/tests/run_from_console.zombie.js create mode 100644 public/novnc/tests/stats.js create mode 100644 public/novnc/tests/test.base64.js create mode 100644 public/novnc/tests/test.display.js create mode 100644 public/novnc/tests/test.helper.js create mode 100644 public/novnc/tests/test.keyboard.js create mode 100644 public/novnc/tests/test.rfb.js create mode 100644 public/novnc/tests/test.util.js create mode 100644 public/novnc/tests/test.websock.js create mode 100644 public/novnc/tests/viewport.css create mode 100644 public/novnc/tests/viewport.html create mode 100644 public/novnc/tests/vnc_perf.html create mode 100644 public/novnc/tests/vnc_playback.html create mode 100644 public/novnc/utils/README.md create mode 100644 public/novnc/utils/b64-to-binary.pl create mode 100644 public/novnc/utils/img2js.py create mode 100644 public/novnc/utils/inflator.partial.js create mode 100644 public/novnc/utils/json2graph.py create mode 100644 public/novnc/utils/launch.sh create mode 100644 public/novnc/utils/parse.js create mode 100644 public/novnc/utils/u2x11 create mode 160000 public/novnc/utils/websockify create mode 100644 public/novnc/vnc.html create mode 100644 public/novnc/vnc_auto.html diff --git a/.gitignore b/.gitignore index fe963c9e..0f2a3caa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -media_import/* +media_import/*.img +media_import/*.size + + diff --git a/media_import/placeholder b/media_import/placeholder new file mode 100644 index 00000000..bb4b9533 --- /dev/null +++ b/media_import/placeholder @@ -0,0 +1,2 @@ +placeholder + diff --git a/php/clonos.php b/php/clonos.php index 7bbf2c27..1716493a 100644 --- a/php/clonos.php +++ b/php/clonos.php @@ -2293,14 +2293,14 @@ class ClonOS ), ); - $result='0 mb'; + $result='0 MB'; foreach($arBytes as $arItem) { if($bytes >= $arItem["VALUE"]) { $result = $bytes / $arItem["VALUE"]; if($round) $result=round($result); - $result = str_replace(".", "," , strval(round($result, 2)))." ".$arItem["UNIT"]; + $result = str_replace(".", "," , strval(round($result, 2)))." ".strtoupper($arItem["UNIT"]); break; } } diff --git a/public/css/styles.css b/public/css/styles.css index a74dc95f..938652ab 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -359,6 +359,16 @@ p.log-p:before { } +.row2col:after { + content: ""; + display: table; + clear: both; +} +.row2col .column { + float: left; + width: 50%; +} + /* Таблицы: http://html5book.ru/examples/demo-tables.html */ @@ -629,6 +639,29 @@ tr.busy.maintenance .ops span { .vnc-grey { color:gray; } +.vnc-wait { + position:absolute; + width:100%; + background-color:white; + opacity:0.7; + top:25px; + bottom:0; + font-weight:bold; + font-size:150%; +} +.vnc-wait .outer { + height:100%; + text-align:center; +} +.vnc-wait .inner { + margin: auto; + height: 30px; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} /* td.ops span.icon-play:before { margin-left:.2em; @@ -1483,16 +1516,14 @@ div.smoothie-chart-tooltip { box-shadow:3px 3px 3px 0px rgba(0,0,0,0.1); line-height: 120%; } -.smoothie-chart-tooltip :nth-child(2):before { - content:'cpu: ' -} -.smoothie-chart-tooltip :nth-child(4):before { - content:'mem: ' -} .smoothie-chart-tooltip :nth-child(2):after, .smoothie-chart-tooltip :nth-child(4):after { content:'%' } td.graph { /*background-color:black;*/ +} + +.row2col div.smoothie-chart-tooltip { + background:gray; } \ No newline at end of file diff --git a/public/dialogs/vnc-bhyve.php b/public/dialogs/vnc-bhyve.php index 7024e8d7..c96e8776 100644 --- a/public/dialogs/vnc-bhyve.php +++ b/public/dialogs/vnc-bhyve.php @@ -7,5 +7,13 @@ +
+
+
+ Please, wait for initialize Virtual Machine +
Нужно найти куда воткнуть событие, что машина инициализирована и вызвать мою функцию, которая закроет перектывающий фон. +
+
+
\ No newline at end of file diff --git a/public/js/clonos.js b/public/js/clonos.js index c6ed9f4f..e143db67 100644 --- a/public/js/clonos.js +++ b/public/js/clonos.js @@ -90,6 +90,7 @@ var clonos={ if(this.manual_close_menu) return; var wdt=$(window).width(); if(wdt<800) $('body').addClass('gadget'); else $('body').removeClass('gadget'); + setTimeout(graphs.onResize,500); }, closerClick:function(event) { @@ -937,6 +938,13 @@ var clonos={ return; } for(id in data) $('#'+id).html(data[id]); + + var razd=location.pathname; + if(['/overview/'].indexOf(razd)!=-1) + { + clonos.createGraphs(); + console.log(data.id); + } } }, @@ -986,7 +994,7 @@ var clonos={ // --- // Если мы в нужной таблице, то рисуем графики - if(data.id=='bhyveslist' || data.id=='jailslist') + if(['bhyveslist','jailslist'].indexOf(data.id)!=-1) { clonos.createGraphs(); } @@ -2643,7 +2651,7 @@ var clonos={ { data.data['vm_ram']=ram.replace(/^\d+([gmt])$/gi,function(orig,lett) { - var a={'m':' MB','g':' GB','t':' TB'}[lett.toLowerCase()]; + var a={'m':' MB','g':' GB','t':' TB'}[lett.toUpperCase()]; //!!! return orig.replace(lett,a); }); }else{ @@ -3044,7 +3052,7 @@ var clonos={ createGraphs:function() { - var grs=$('td.graph'); + var grs=$('.graph'); for(n=0,nl=grs.length;n-1) larr.splice(res,1); - - //console.log(name,cpu,mem); + var item=items[i]; + var gr=this.list[item.name]; + var date = new Date(); + date.setTime(item.time*1000); + if(gr) + { + var cpu,mem; + if(typeof item.pcpu!='undefined') + { + gr.line1.append(date.getTime(), item.pcpu); + } + if(typeof item.pmem!='undefined') + { + gr.line2.append(date.getTime(), item.pmem); + } + }else{ + var gr=this.list[item.name+'-pcpu']; + { + if(gr) gr.line1.append(date.getTime(), item.pcpu); + } + var gr=this.list[item.name+'-pmem']; + { + if(gr) gr.line1.append(date.getTime(), item.pmem); + } + } } } + }else{ + + var larr=[]; + for(l in this.list) + larr.push(this.list[l].name) + + if(typeof msg=='object') + { + for(n in msg) + { + var inf=msg[n]; + var name=inf['name']; + var gr=this.list[name]; + var date = new Date(); + date.setTime(inf.time*1000); + if(gr) + { + var cpu=inf.pcpu, mem=inf.pmem; + gr.line1.append(date.getTime(), cpu); + gr.line2.append(date.getTime(), mem); + var res=larr.indexOf(name); + if(res>-1) larr.splice(res,1); + }else{ + var nname=name+'-pcpu'; + var gr=this.list[nname]; + { + if(gr) gr.line1.append(date.getTime(), inf.pcpu); + var res=larr.indexOf(nname); + if(res>-1) larr.splice(res,1); + } + var nname=name+'-pmem'; + var gr=this.list[nname]; + { + if(gr) gr.line1.append(date.getTime(), inf.pmem); + var res=larr.indexOf(nname); + if(res>-1) larr.splice(res,1); + } + } + } + } + + for(n=0,nl=larr.length;n'); + var view; + var cl=$(el_parent).attr('class'); + var res=cl.match(/v-([a-z]+)/); + if(res!=null) + { + view=res[1]; + }else{ + view='white'; + } + var varr=this.graphView[view]; + this.line1 = new TimeSeries({resetBounds:false,resetBoundsInterval:3000}); this.line2 = new TimeSeries({resetBounds:false,resetBoundsInterval:3000}); var back_color='rgba(0,0,0,0.02)'; - var line1_color='rgb(17,125,187)'; //'blue'; //'rgb(0,0,255)'; - var line1_fillStyle='rgba(17,125,187,0.03)'; //'rgba(241, 240, 255, 0.4)'; - var line2_color='rgb(149,40,180)'; //'green'; //'rgb(0,255,0)'; - var line2_fillStyle='rgba(149,40,180,0.03)'; //'rgba(222,245,222,0.4)'; + var line1_color=varr.colors.line1_color; + var line1_fillStyle=varr.colors.line1_fillStyle; + var line2_color=varr.colors.line2_color; + var line2_fillStyle=varr.colors.line2_fillStyle; - this.smoothie = new SmoothieChart({interpolation:'bezier',grid:{fillStyle:back_color,sharpLines:true,strokeStyle:'transparent',borderVisible:true},labels:{fontSize:8,disabled:true,fillStyle:'#000000',precision:0},millisPerPixel:1000,enableDpiScaling:true,tooltip:true,maxValue:100,minValue:0}); + this.smoothie = new SmoothieChart(varr.view); - this.smoothie.addTimeSeries(this.line1, { strokeStyle: line1_color, lineWidth:0.5, fillStyle:line1_fillStyle }); //, fillStyle: 'rgba(0, 255, 0, 0.4)' - this.smoothie.addTimeSeries(this.line2, { strokeStyle: line2_color, lineWidth:0.5, fillStyle:line2_fillStyle }); //, fillStyle: 'rgba(255, 0, 255, 0.3)' + this.smoothie.addTimeSeries(this.line1, { strokeStyle: line1_color, lineWidth:varr.lineWidth, fillStyle:line1_fillStyle, tooltipLabel:this.tooltip1+':' }); + this.smoothie.addTimeSeries(this.line2, { strokeStyle: line2_color, lineWidth:varr.lineWidth, fillStyle:line2_fillStyle, tooltipLabel:this.tooltip2+':' }); this.smoothie.streamTo(document.getElementById('g-'+this.name), 1000); graphs.list[this.name]=this; diff --git a/public/novnc/CONTRIBUTING.md b/public/novnc/CONTRIBUTING.md new file mode 100644 index 00000000..4bcea104 --- /dev/null +++ b/public/novnc/CONTRIBUTING.md @@ -0,0 +1,54 @@ +How to contribute to noVNC +========================== + +We accept code via pull requests on GitHub. There are several guidelines that +we expect contributors submitting code requests to follow. If you have issues +following any of these guidelines, feel free to drop us a line by leaving a +comment in the code request or sending us an email. + +Contributing Guidelines +----------------------- + +* While we don't have an official coding style guide, please try to follow + the general coding style of the existing code. +** Use four spaces instead of tabs +** prefix private variables and functions with an `_` + +* Please try to include unit tests for your code. For instance, if you + introduce a new encoding, add a test to `tests/test.rfb.js` under the + "Encoding Handlers" section (basically, input a small pattern in your + encoding and make sure the pattern gets displayed correctly). If you + fix a bug, try to add a unit test that would have caught that bug + (if possible -- some bugs, especially visual ones, are hard to test for). + +* Squash your commits down in to a clean commit history. For instance, there + should not be "cleanup" commits where you fix issues in previous commits in + the same pull request. Before you go to commit, use `git rebase -i` to + squash these changes into the relevant commits. For instance, a good commit + history might look like "Added support for FOO encoding, Added support for + BAR message, Placed Button in UI to Trigger BAR" (where each comma denotes + a separate commit). + +* Add both a title and description to your commit, if possible. Place more + detail on what you did in the description. + +Running the unit tests +---------------------- + +There are two ways to run the unit tests. For both ways, you should first run +`npm install` (not as root). + +The first way to run the tests is to run `npm test`. This will run all the +tests in the headless PhantomJS browser (which uses WebKit). + +The second way to run the tests is using the `tests/run_from_console.js` file. +This way is a bit more flexible, and can provide more information about what +went wrong. To run all the tests, simply run `tests/run_from_console.js`. +To run a specific test file, you can use the `-t path/to/test/file.js` option. +If you wish to simply generate the HTML for the test, use the `-g` option, and +the path to the temporary HTML file will be written to standard out. To open +this file in your default browser automatically, pass the `-o` option as well. +More information can be found by passing the `--help` or `-h` option. + + +Thanks, and happy coding! diff --git a/public/novnc/LICENSE.txt b/public/novnc/LICENSE.txt new file mode 100644 index 00000000..f217929f --- /dev/null +++ b/public/novnc/LICENSE.txt @@ -0,0 +1,84 @@ +noVNC is Copyright (C) 2011 Joel Martin + +The noVNC core library files are licensed under the MPL 2.0 (Mozilla +Public License 2.0). The noVNC core library is composed of the +Javascript code necessary for full noVNC operation. This includes (but +is not limited to): + + include/base64.js + include/des.js + include/display.js + include/input.js + include/keysym.js + include/logo.js + include/playback.js + include/rfb.js + include/ui.js + include/util.js + include/websock.js + include/webutil.js + +The HTML, CSS, font and images files that included with the noVNC +source distibution (or repository) are not considered part of the +noVNC core library and are licensed under more permissive licenses. +The intent is to allow easy integration of noVNC into existing web +sites and web applications. + +The HTML, CSS, font and image files are licensed as follows: + + *.html : 2-Clause BSD license + + include/*.css : 2-Clause BSD license + + include/Orbitron* : SIL Open Font License 1.1 + (Copyright 2009 Matt McInerney) + + images/ : Creative Commons Attribution-ShareAlike + http://creativecommons.org/licenses/by-sa/3.0/ + +Some portions of noVNC are copyright to their individual authors. +Please refer to the individual source files and/or to the noVNC commit +history: https://github.com/kanaka/noVNC/commits/master + +The are several files and projects that have been incorporated into +the noVNC core library. Here is a list of those files and the original +licenses (all MPL 2.0 compatible): + + include/base64.js : MPL 2.0 + + include/des.js : Various BSD style licenses + + include/chrome-app/tcp-stream.js + : Apache 2.0 license + + utils/websockify + utils/websocket.py : LGPL 3 + + utils/inflator.partial.js + include/inflator.js : MIT (for pako) + +Any other files not mentioned above are typically marked with +a copyright/license header at the top of the file. The default noVNC +license is MPL-2.0. + +The following license texts are included: + + docs/LICENSE.MPL-2.0 + docs/LICENSE.LGPL-3 and + docs/LICENSE.GPL-3 + docs/LICENSE.OFL-1.1 + docs/LICENSE.BSD-3-Clause (New BSD) + docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD) + docs/LICENSE.zlib + docs/LICENSE.Apache-2.0 + docs/LICENSE.pako + +Or alternatively the license texts may be found here: + + http://www.mozilla.org/MPL/2.0/ + http://www.gnu.org/licenses/lgpl.html and + http://www.gnu.org/licenses/gpl.html + http://scripts.sil.org/OFL + http://en.wikipedia.org/wiki/BSD_licenses + http://www.gzip.org/zlib/zlib_license.html + http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/public/novnc/README.md b/public/novnc/README.md new file mode 100644 index 00000000..59a72b10 --- /dev/null +++ b/public/novnc/README.md @@ -0,0 +1,132 @@ +## noVNC: HTML5 VNC Client + +[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=master)](https://travis-ci.org/kanaka/noVNC) + +### Description + +noVNC is a HTML5 VNC client that runs well in any modern browser +including mobile browsers (iPhone/iPad and Android). + +Many companies/projects have integrated noVNC including [Ganeti Web +Manager](http://code.osuosl.org/projects/ganeti-webmgr), +[OpenStack](http://www.openstack.org), +[OpenNebula](http://opennebula.org/), and +[LibVNCServer](http://libvncserver.sourceforge.net). See [the Projects +and Companies wiki +page](https://github.com/kanaka/noVNC/wiki/ProjectsCompanies-using-noVNC) +for a more complete list with additional info and links. + +### News/help/contact + +Notable commits, announcements and news are posted to +@noVNC + +If you are a noVNC developer/integrator/user (or want to be) please +join the noVNC +discussion group + +Bugs and feature requests can be submitted via [github +issues](https://github.com/kanaka/noVNC/issues). If you are looking +for a place to start contributing to noVNC, a good place to start +would be the issues that are marked as +["patchwelcome"](https://github.com/kanaka/noVNC/issues?labels=patchwelcome). + +If you want to show appreciation for noVNC you could donate to a great +non-profits such as: [Compassion +International](http://www.compassion.com/), [SIL](http://www.sil.org), +[Habitat for Humanity](http://www.habitat.org), [Electronic Frontier +Foundation](https://www.eff.org/), [Against Malaria +Foundation](http://www.againstmalaria.com/), [Nothing But +Nets](http://www.nothingbutnets.net/), etc. Please tweet @noVNC if you do. + + +### Features + +* Supports all modern browsers including mobile (iOS, Android) +* Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG +* WebSocket SSL/TLS encryption (i.e. "wss://") support +* 24-bit true color and 8 bit colour mapped +* Supports desktop resize notification/pseudo-encoding +* Local or remote cursor +* Clipboard copy/paste +* Clipping or scolling modes for large remote screens +* Easy site integration and theming (3 example themes included) +* Licensed under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/) + +### Screenshots + +Running in Chrome before and after connecting: + +  + +See more screenshots here. + + +### Browser Requirements + +* HTML5 Canvas (with createImageData): Chrome, Firefox 3.6+, iOS + Safari, Opera 11+, Internet Explorer 9+, etc. + +* HTML5 WebSockets and Typed Arrays + +* Fast Javascript Engine: this is not strictly a requirement, but + without a fast Javascript engine, noVNC might be painfully slow. + +* See the more detailed [browser compatibility wiki page](https://github.com/kanaka/noVNC/wiki/Browser-support). + + +### Server Requirements + +Unless you are using a VNC server with support for WebSockets +connections (such as +[x11vnc/libvncserver](http://libvncserver.sourceforge.net/), +[QEMU](http://www.qemu.org/), or +[PocketVNC](http://www.pocketvnc.com/blog/?page_id=866)), you need to +use a WebSockets to TCP socket proxy. There is a python proxy included +('websockify'). + + +### Quick Start + +* Use the launch script to start a mini-webserver and the WebSockets + proxy (websockify). The `--vnc` option is used to specify the location of + a running VNC server: + + `./utils/launch.sh --vnc localhost:5901` + +* Point your browser to the cut-and-paste URL that is output by the + launch script. Enter a password if the VNC server has one + configured. Hit the Connect button and enjoy! + + +### Other Pages + +* [Encrypted Connections](https://github.com/kanaka/websockify/wiki/Encrypted-Connections). How to setup websockify so that you can use encrypted connections from noVNC. + +* [Advanced Usage](https://github.com/kanaka/noVNC/wiki/Advanced-usage). Starting a VNC server, advanced websockify usage, etc. + +* [Integrating noVNC](https://github.com/kanaka/noVNC/wiki/Integration) into existing projects. + +* [Troubleshooting noVNC](https://github.com/kanaka/noVNC/wiki/Troubleshooting) problems. + + +### Authors/Contributors + +* Core team: + * [Joel Martin](https://github.com/kanaka) + * [Samuel Mannehed](https://github.com/samhed) (Cendio) + * [Peter Åstrand](https://github.com/astrand) (Cendio) + * [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack) + +* Notable contributions: + * UI and Icons : Chris Gordon + * Original Logo : Michael Sersen + * tight encoding : Michael Tinglof (Mercuri.ca) + +* Included libraries: + * as3crypto : Henri Torgemane (code.google.com/p/as3crypto) + * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net) + * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs) + * Pako : Vitaly Puzrin (https://github.com/nodeca/pako) diff --git a/public/novnc/docs/LICENSE.Apache-2.0 b/public/novnc/docs/LICENSE.Apache-2.0 new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/public/novnc/docs/LICENSE.Apache-2.0 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/public/novnc/docs/LICENSE.BSD-2-Clause b/public/novnc/docs/LICENSE.BSD-2-Clause new file mode 100644 index 00000000..9d66ec91 --- /dev/null +++ b/public/novnc/docs/LICENSE.BSD-2-Clause @@ -0,0 +1,22 @@ +Copyright (c) , +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/public/novnc/docs/LICENSE.BSD-3-Clause b/public/novnc/docs/LICENSE.BSD-3-Clause new file mode 100644 index 00000000..e160466c --- /dev/null +++ b/public/novnc/docs/LICENSE.BSD-3-Clause @@ -0,0 +1,24 @@ +Copyright (c) , +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/public/novnc/docs/LICENSE.GPL-3 b/public/novnc/docs/LICENSE.GPL-3 new file mode 100644 index 00000000..94a04532 --- /dev/null +++ b/public/novnc/docs/LICENSE.GPL-3 @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/public/novnc/docs/LICENSE.LGPL-3 b/public/novnc/docs/LICENSE.LGPL-3 new file mode 100644 index 00000000..65c5ca88 --- /dev/null +++ b/public/novnc/docs/LICENSE.LGPL-3 @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/public/novnc/docs/LICENSE.MPL-2.0 b/public/novnc/docs/LICENSE.MPL-2.0 new file mode 100644 index 00000000..14e2f777 --- /dev/null +++ b/public/novnc/docs/LICENSE.MPL-2.0 @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/public/novnc/docs/LICENSE.OFL-1.1 b/public/novnc/docs/LICENSE.OFL-1.1 new file mode 100644 index 00000000..77b17316 --- /dev/null +++ b/public/novnc/docs/LICENSE.OFL-1.1 @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/novnc/docs/LICENSE.pako b/public/novnc/docs/LICENSE.pako new file mode 100644 index 00000000..e6c9e5a5 --- /dev/null +++ b/public/novnc/docs/LICENSE.pako @@ -0,0 +1,21 @@ +(The MIT License) + +Copyright (C) 2014 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/public/novnc/docs/LICENSE.zlib b/public/novnc/docs/LICENSE.zlib new file mode 100644 index 00000000..ca151681 --- /dev/null +++ b/public/novnc/docs/LICENSE.zlib @@ -0,0 +1,27 @@ +Copyright (c) , +All rights reserved. + +This software is provided 'as-is', without any express +or implied warranty. In no event will the authors be +held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software +for any purpose, including commercial applications, +and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be + misrepresented; you must not claim that you + wrote the original software. If you use this + software in a product, an acknowledgment in + the product documentation would be appreciated + but is not required. + +2. Altered source versions must be plainly marked + as such, and must not be misrepresented as + being the original software. + +3. This notice may not be removed or altered from + any source distribution. + diff --git a/public/novnc/docs/VERSION b/public/novnc/docs/VERSION new file mode 100644 index 00000000..b6160487 --- /dev/null +++ b/public/novnc/docs/VERSION @@ -0,0 +1 @@ +0.6.2 diff --git a/public/novnc/docs/flash_policy.txt b/public/novnc/docs/flash_policy.txt new file mode 100644 index 00000000..df325c0d --- /dev/null +++ b/public/novnc/docs/flash_policy.txt @@ -0,0 +1,4 @@ +Manual setup: + +DATA="echo \'\'" +/usr/bin/socat -T 1 TCP-L:843,reuseaddr,fork,crlf SYSTEM:"$DATA" diff --git a/public/novnc/docs/links b/public/novnc/docs/links new file mode 100644 index 00000000..31544ce0 --- /dev/null +++ b/public/novnc/docs/links @@ -0,0 +1,76 @@ +New tight PNG protocol: + http://wiki.qemu.org/VNC_Tight_PNG + http://xf.iksaif.net/blog/index.php?post/2010/06/14/QEMU:-Tight-PNG-and-some-profiling + +RFB protocol and extensions: + http://tigervnc.org/cgi-bin/rfbproto + +Canvas Browser Compatibility: + http://philip.html5.org/tests/canvas/suite/tests/results.html + +WebSockets API standard: + http://www.whatwg.org/specs/web-apps/current-work/complete.html#websocket + http://dev.w3.org/html5/websockets/ + http://www.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-00.txt + +Browser Keyboard Events detailed: + http://unixpapa.com/js/key.html + +ActionScript (Flash) WebSocket implementation: + http://github.com/gimite/web-socket-js + +ActionScript (Flash) crypto/TLS library: + http://code.google.com/p/as3crypto + http://github.com/lyokato/as3crypto_patched + +TLS Protocol: + http://en.wikipedia.org/wiki/Transport_Layer_Security + +Generate self-signed certificate: + http://docs.python.org/dev/library/ssl.html#certificates + +Cursor appearance/style (for Cursor pseudo-encoding): + http://en.wikipedia.org/wiki/ICO_(file_format) + http://www.daubnet.com/en/file-format-cur + https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property + http://www.fileformat.info/format/bmp/egff.htm + +Icon/Cursor file format: + http://msdn.microsoft.com/en-us/library/ms997538 + http://msdn.microsoft.com/en-us/library/aa921550.aspx + http://msdn.microsoft.com/en-us/library/aa930622.aspx + + +RDP Protocol specification: + http://msdn.microsoft.com/en-us/library/cc240445(v=PROT.10).aspx + + +Related projects: + + guacamole: http://guacamole.sourceforge.net/ + + - Web client, but Java servlet does pre-processing + + jsvnc: http://code.google.com/p/jsvnc/ + + - No releases + + webvnc: http://code.google.com/p/webvnc/ + + - Jetty web server gateway, no updates since April 2008. + + RealVNC Java applet: http://www.realvnc.com/support/javavncviewer.html + + - Java applet + + Flashlight-VNC: http://www.wizhelp.com/flashlight-vnc/ + + - Adobe Flash implementation + + FVNC: http://osflash.org/fvnc + + - Adbove Flash implementation + + CanVNC: http://canvnc.sourceforge.net/ + + - HTML client with REST to VNC python proxy. Mostly vapor. diff --git a/public/novnc/docs/notes b/public/novnc/docs/notes new file mode 100644 index 00000000..036cd510 --- /dev/null +++ b/public/novnc/docs/notes @@ -0,0 +1,5 @@ +Rebuilding inflator.js + +- Download pako from npm +- Install browserify using npm +- browserify utils/inflator.partial.js -o include/inflator.js -s inflator diff --git a/public/novnc/docs/release.txt b/public/novnc/docs/release.txt new file mode 100644 index 00000000..3e036354 --- /dev/null +++ b/public/novnc/docs/release.txt @@ -0,0 +1,34 @@ +- Decide a new version number X.Y.Z (follow SemVer) +- Update version in package.json +- Update version in docs/VERSION +- Commit the change with a commit like "Release X.Y.Z" +- Add a new release on GitHub called "vX.Y.Z", and populate it with + release notes of the following form (where A.B.C is the last release): + +Major Changes Since A.B.C +========================= + +*Insert warnings here about incompatibilities* + +*Thanks to all the contributors who filed bugs, added features, and fixed bugs +during this release :tada:* + +App-visible Changes +------------------- + +- *feature* a feature which improves the app usage (#PRNUM) +- *bugfix* a bug fix which fixes the app usage (#PRNUM) +- *refactor* a refactor which changes the app usage (#PRNUM) + +Library-visible Changes +----------------------- + +- *feature* a feature which improves the noVNC APIs (#PRNUM) +- *bugfix* a bug fix which fixes the noVNC APIs (#PRNUM) +- *refactor* a refactor which changes the noVNC APIs (#PRNUM) + +App-internals Changes +--------------------- + +- *bugfix* a bug fix with affects the internals of noVNC only (#PRNUM) +- *refactor* a refactor which affects the internals of noVNC only (#PRNUM) diff --git a/public/novnc/docs/rfb_notes b/public/novnc/docs/rfb_notes new file mode 100644 index 00000000..643e16c0 --- /dev/null +++ b/public/novnc/docs/rfb_notes @@ -0,0 +1,147 @@ +5.1.1 ProtocolVersion: 12, 12 bytes + + - Sent by server, max supported + 12 ascii - "RFB 003.008\n" + - Response by client, version to use + 12 ascii - "RFB 003.003\n" + +5.1.2 Authentication: >=4, [16, 4] bytes + + - Sent by server + CARD32 - authentication-scheme + 0 - connection failed + CARD32 - length + length - reason + 1 - no authentication + + 2 - VNC authentication + 16 CARD8 - challenge (random bytes) + + - Response by client (if VNC authentication) + 16 CARD8 - client encrypts the challenge with DES, using user + password as key, sends resulting 16 byte response + + - Response by server (if VNC authentication) + CARD32 - 0 - OK + 1 - failed + 2 - too-many + +5.1.3 ClientInitialisation: 1 byte + - Sent by client + CARD8 - shared-flag, 0 exclusive, non-zero shared + +5.1.4 ServerInitialisation: >=24 bytes + - Sent by server + CARD16 - framebuffer-width + CARD16 - framebuffer-height + 16 byte PIXEL_FORMAT - server-pixel-format + CARD8 - bits-per-pixel + CARD8 - depth + CARD8 - big-endian-flag, non-zero is big endian + CARD8 - true-color-flag, non-zero then next 6 apply + CARD16 - red-max + CARD16 - green-max + CARD16 - blue-max + CARD8 - red-shift + CARD8 - green-shift + CARD8 - blue-shift + 3 bytes - padding + CARD32 - name-length + + CARD8[length] - name-string + + + +Client to Server Messages: + +5.2.1 SetPixelFormat: 20 bytes + CARD8: 0 - message-type + ... + +5.2.2 FixColourMapEntries: >=6 bytes + CARD8: 1 - message-type + ... + +5.2.3 SetEncodings: >=8 bytes + CARD8: 2 - message-type + CARD8 - padding + CARD16 - numer-of-encodings + + CARD32 - encoding-type in preference order + 0 - raw + 1 - copy-rectangle + 2 - RRE + 4 - CoRRE + 5 - hextile + +5.2.4 FramebufferUpdateRequest (10 bytes) + CARD8: 3 - message-type + CARD8 - incremental (0 for full-update, non-zero for incremental) + CARD16 - x-position + CARD16 - y-position + CARD16 - width + CARD16 - height + + +5.2.5 KeyEvent: 8 bytes + CARD8: 4 - message-type + CARD8 - down-flag + 2 bytes - padding + CARD32 - key (X-Windows keysym values) + +5.2.6 PointerEvent: 6 bytes + CARD8: 5 - message-type + CARD8 - button-mask + CARD16 - x-position + CARD16 - y-position + +5.2.7 ClientCutText: >=9 bytes + CARD8: 6 - message-type + ... + + +Server to Client Messages: + +5.3.1 FramebufferUpdate + CARD8: 0 - message-type + 1 byte - padding + CARD16 - number-of-rectangles + + CARD16 - x-position + CARD16 - y-position + CARD16 - width + CARD16 - height + CARD16 - encoding-type: + 0 - raw + 1 - copy rectangle + 2 - RRE + 4 - CoRRE + 5 - hextile + + raw: + - width x height pixel values + + copy rectangle: + CARD16 - src-x-position + CARD16 - src-y-position + + RRE: + CARD32 - N number-of-subrectangles + Nxd bytes - background-pixel-value (d bits-per-pixel) + + ... + +5.3.2 SetColourMapEntries (no support) + CARD8: 1 - message-type + ... + +5.3.3 Bell + CARD8: 2 - message-type + +5.3.4 ServerCutText + CARD8: 3 - message-type + + + + + diff --git a/public/novnc/docs/rfbproto-3.3.pdf b/public/novnc/docs/rfbproto-3.3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..56b876436a9b18e3fbc0001efa9c3dba498e3558 GIT binary patch literal 110778 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
  • soRRg(4fNFaOUHNL^!mU2PjY+6DhGfn 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 UdT@3HIk zWRx(oa4@n1GfG(KIT(o<8CV+{2?!wBJJ=cNSt2>7oqS!fU*bSH)4o>QH%nX{Md=g^ zjtoxKtYz1TaXpdI!B4HOABrI#YTxc8MLeWU*zEsm_07IRdz>3cA+(Nw0cL^-G`yCq z9Za=eTX{g&J(#pyMSfV_h(G@rM1|NS*nB}OctIVG5$O(+2uemD_8SuEOn=&8_O+qP z-$))otN0Y@Ln+^9z9^m%zrZGQj4+=HCV-teObi(m5XOIQsYSnRq^Rl2L%zl6D zGc|cm^h>4FIpk|@JSHaBV-KLOA(DaM2ZSehQ7}(XZHBXV^oUS|kNM>8*Em=}K~6q8 zWUB~hdGC4*A{b0z+qf;!Cd2I(-VCjuz*7RV%+72%Hv<%dG05b1xOP7x{FZacTeeGK=F62SzYgB}V^9vb=vkQA_!GCEVXe3A#<@6ib=JFJAZXq` zEkFO$aL!J@l>WikX(U}w*L|MJb8X#-556N`+UW1;8UdWGAXO7zEM_u2$q&gl-`(-> zO2t;x3Yn>b551vOeN9mtDLwIxZ}|K~n9PcZLMngjvS4-5Yd0b?gDU|22xvWSm=#w@ z-*Y9Jf6WCj*Obs3XofCG97jc{sa>SQ53%iU$0XC>U~pT;m0bs82<6+KW);q=vgTAV zH+HZFRZY+>Z^&h->G=9ghnfs?&xvR6x_&|OS3wq!MK3nOC#X{x45j?#hRY+f#iIK0 zXvuuwmL?qs`PwOMj7(TJl@OthiA4$qXh;o$r94yU8{IUT_DyZBH~{EKuV*uHJeESzesSrFH!1dPRlW z1b5M2kio7$ILj-ym?G5Fz58mk#o}j)XnHIqjv8O3G_4sW|vJ2$K z7C+%5eVxm|xXCM|ICGHbV28xZs(dSRuh^WCF`v<~7I|D6MvBGYj;#6Mcx2|zITmA) zo>@BkQGK_17#|k7&&irGLqE|Y^1(p|Uc-HXkw9)gGjG}gx6s}u2Y5VuBGWh8A^t3I zLDsOIt7DY0YhGOGqgmY0VXN446UNE&YR|5e-!awr?Igp4+$XiD0@*FXAm_X0 zg#EI00w#xbA`{f+#8=h0$JiE1L(9kY!;jO7bt+vEH_IPF=6pGw-MEtP5?9M5n^5#- zyq`gwJK*kR<$&EpLj06BcHkAbL>nJ_X3;t79T(tsw|*hj3G>)YZ@*k~{$R4YV_-j$ z9gR!Y^NX1#;Jfs7tV(}JyQCCSch0F)sJw*??w#n|Pc2_DE%sK_qc zKAxgeXdgD6W1lL$JC*pPqoq^L z>B=elDLLyTzFK|??3(D35;5(Tn(w_LqIRZQM{8(A6ti&fvGzfAm>fB&}3 ztn3^}|FW_EhozAim0fI%z>M;GCPqk%az=(`dLq`&U=5}>1P3zq!L6sBGly@D?dy{T9c^ z_ZGy=1b%z{qXS_u*S~t=0CW69ddmfGFGkTf^xGTRga4)x#lNP1T`=>1ulV12h4~+t z|7&7N8MRwv!Vb9Q3rIYGo_(k-v()6#sxyN=E#O6hh$1K9PZxMRsH<@VMC%iy$NDSB z!u3|u5v*B9jMZa7i2WZbAJXXDoZ4FfeyU!w3Ef7EZoFaAP($*Y;PmS%Zdyzz2BW|) z;3gp!sYJY?y;298_=wfKwb3d&AeMs{aB|=w4u}os@k@ai;ou4)0xSLiG?LOMNHj?} zbmY*%p&@;mk;E5wa1tv=DhK&i#Z8$?(K7dFN~l?@Zvpl)>gfj@a& ziaAm*$`=8i(6;Vz?BkQe+eNF*7PL7)4b>|FQpQ=%s8X4wu@RchHS4tEr(stfhBza& z&ppt!ze6;ZYjJ;-n)q;33DMz7lt|TxH;f8~&-tlDkB7gLYErlLox1;%(a=_w-8MsW zmak6Nb(_p9omQf1>A7$MHBA9wt#^j@R^;>6$;Dk7 zj6ihvK(sAXhF=)(7rB}PcSmG5jDes(s@j*YO-Yf1TFlrDp_6~Ipe8{vKMa!mlB_$| zWrCdR^Gbx^V92FfxcH5~toDorq~CSFi{gej#+2)Y93GuzrzEZN{{D=Z4+AdAeC9K9 z+Of=|MXG^<4RP$1=)8dKe$#eu_fk*Nr zVAo-xJbrt#w4;>?J!U(>q+>nvJ>`1VbB(yM`ecCfLx&bOo#MHBy+PJelVlbh{7v+nPN|7SJ#|2t3oFMjwxQNcf^{{M>$ z{!+$2uj)Ts!1GSWUjK?LZfryBKB;MajKqtJ@BeL zTYFkc^=h>VTkxtlWAwbd8BNqVLv1S4aKy7->$HveHjY=(eZiu=9mja|bLmm1xzqKr zbFJx_rAd8B&3f(mu++=_k=;i?%W9$P^@6kW@u*SfInrypGQT4<2_^x1f34%GrM~y) zFox4-b}!J4>xH;fV!k}HW~x(SHQv)~?nF?sWBEcO0(`@8T-gjQbzI4$ z5BKt_SKF2`ycaj&>&3yv{l&vY#A~*;vB_Gem;2PhJFNSeR>B^mLnBQsoi*Yn)b4I| zXU}cuWuw#4hjWMPi`Jzh+6spLVfrtQnP^i794l_$zBk|X)YIII+9Hr}6@pY2~daq>_yhq*ME zp%T%_rT%tjD`ybSHI!f(PZL?gifKx1WvJq{{CKUT#-K&?t+ULB`9+OXaJ9brMfET! zc~LQ;nBeI8@RK=WV!eUjvOt^L3={*w1UipO2dclUN?6IY%ecE zTNlvm(zKSubJT;OZl>mDZ{y)~nz#I;YRJp*tHbGB%YjE`fLAmWwe3@zEL(zA+}u9I*!qQ$aEr%be57C0|K+!9F5L=kU;!|VCU*6eg^?n~pvv*MjNN4U!6 zAg>niG<2fDIyoVc!45%mx@r4Vf@Xf#(@3{@>@sn&;;3~g^~c`uq{6fO-b5?b<+fR8 z`Q^-_V8iLPlNvgy6`C}^m5~T~>47rst22%8)nd0NzWp7!*Ejug9Cs%r&-~6@%5q2QwY(;F6Y+)Y8^g=ZA@zl%<9!!eztn{^^GgQU1mky%zuGgv7>h=P4i(;iA)yxNqdT)$VC`!wjeaZ50SConG6eC{Z5C~kwL*g)R* zj_f6e*+`iJR8uLnQdEIx)^3U_?@w9J?EYvhg5-gPfVs4pd!R~z@#arW`F)FM=9sU| zQcg9n#Yz^?VH-4qV}=CM<|&=lY^hZi{Mm^g*4w{C8m8?)8oHq4+d?zxwq|sQ1y=s0LbIp%wGKwWYmc zwMnh3-AS89T%9ZC0s^zwGkvxI9*o=SYg6_*507kA7gLS8!y)Fn&FNj|T*=Mk?=?@O zYStZ3holS{nF1^B_l%bK9Z!!Or#dgYnJ*7L9`~HDr9)M(x1(xa_7^&@*R5;M%&)Jb zYo4=%KKnlNxW(su%l7glsR9<_KP3_g?E5kEIfm~sBkPxc87Qc^lqq~ECi@r-w%AmX zY^&)KN;_WV@CdZl)yG+3kM41uTAamp;Y#j(N3Zpb zrIfz-vhGLm9$iIoNcG9pr~v-gXuiC9NI=p%d-me9Jr2&|wVg%?m=!x%G>10E)amou zHd#0}wdf(OqFJVX1!d>+^6usjk}N6kJ^zmMn|UgsYkBuZP|wPqJd%kqv>{S7#g5$y z(LR;PF~B8_s6C*0G#nagBfpiD>>hIE3G%rs_#9>(^z5!Q1w|IP{C1=1arc#4UUqTz zebnF*f$T#1SF1+j*$PpYG=skonp8-2+XcBb7LWFgqb(A1Zk{(#tM*0OZAy%sL3@hw zJAy!m#akS-g4Q#Pi5EnNiL^oToti&2-=2-s|8U*lhs+?8f)jH&-0-XSi$*;q{TG)L zoOI|zbls$6kUJnE?LaR5vdD%V)Ey>YdP6zR^`PqAcQ$=@R}%@GalL~x#)oEI%oOeP zb=!mDt{SGX^FlpVvzjjR8070{B91ngv@#4sU49%j#w7FpmjV$bJlDAO|Dp9 zyv@Ji2G2O%9MjZf6vTT%{Bm7@yeSR}60#-^=xa8;RG4eFjYV)hOP-zo`XX><`rQ)o zm*uTRpu5DCBrw9h$gYZbtxxV#f;ShpI{cQ&RMtHO;u)UAi9h{t#~{-9zMXM&!4d?WbG0I!&P>9g5%^f^LqpnduPw76k4AkqWBCKOwrQ zP6Fs#MWJwE~frW3EU5f7ipFH-sz=rvi(+(l!dZRQ$Bq{G*wE>~!B<0T*{~C9uMOa|Z=| z?VOubV{i|fw7P<#(DlwZQHdYyKE{|N^ZEKbJmU~S@mg8XPKAHAm(#su;G$S~?4E2C z*ib0Jy$2JsorMl0J?5k{NvsugkKH+c({610%yHZRalODg?&t)O8Mq)UBH{7)J7Q5g z6JeU&oQwig&>{6udUU9(^qU3xuO$e09k)F;sB-tk)!%SLoL_F~ROC!*P<95#A7JmX=riQwOnIzDEaW2_ zov=H{&Sk((j&>9UAqVy0Ps2(>BJ6alK$Vb1*}_!4gdC1gk6sLO`=yCq!olxFFta8`Ais}>~)Jhz+3j|ZM^sJkn2VA%xs5 ziEuH6X&bWW{$m^v^FvNqE`3b@TWP?Fy_^&7V;Z2araV5gI;%I?l~Q60r&BP#-Z(i8 zyDna)w)I`)pAus-jS=q-T2x>|G-^=8Eb9KTZWq%pf*9Ttd_^910!7|zYRrM_K(Y#@ ze6kLmG<-}RL?X=Kq7Q}>tz>ST=>#kl11Quh^2SmpkdM)9Z;GwfJi%rwXe5fP1~vFf zPRu{tEGTMU4a#s3BR7yjEC=bKd`}wk`x(DitB>c(0k0Y@H(I>xk(rn7!2>TBwHWf- z(vpO?^W;(w0&uFFw<|@MGn>&# z=R5Djr{2zymAS7)dydq)1QPGRGAfnDEWZriV|#C>r^i%8J4Foc{&x%u6m`4LJ{!r= zc|OXPm6M2oYt4~GgnY@a3Lu1i&0$L{*dC*-N0kfPgF4oUet<#+wz87a8xngv^WH6bb(>CxS~BEOhm+> z?bDa6!Lb3Nr-JAwMYq()v6UWoRW_Qyg#~f=E)?T)XbTxuS*#T9+8>?}s*NQWZ@Fmg^C1;$_qhvPnB>T7G2N8@+WIQV0ctzwvkFH2-m z3=#&R6HTP9ZnA#$_AbHo!zNOXPKB>$@9hL&sM~&x@rzdGp|W6kR+1;gn&r)4;`T}p z@kyjB%*lRhH=*6NiD00n9Y22#c6Z@^M}P7zPE)@*N!boAP`aW^o)H_LrWsoFy&B-= z2Oukk7Mi9R(!@<59IpwIAHsq9|7>Pe-h6QXg+w!r@P<@LYXcTMI&GOK^oLZan=U`; z$Md1bw^0;gFDHKp_Q7#Y@s+nx>ylw{T)MVaUO83PjwxPh*9kTwgiVFO?b71B-vI4;dtGO03 zD|@tiHW^mbKN#6VEc^v$W~_`A#s2IB~tCEBm!d@n3UV*^*m63yN_rp+HHrS6Xz zFcBpsO9EFhDl35t#XZ+rB+AVmm7*ld9UhEJ^4qj)9l6Zye@=J_r*VR-`tJlXnmWee z)&@ddR-{)0j#|eQb_6<7l$>BfqC9j0+=DhBp290%-k1`B}IADe~ zA1PpJ2){i2yH(`<TxpgbVc;rJ}KJP-T29Eug>qZj1VO zb_+z=9H<}J9h3kD5gP)s({z>Q_jbe#rgXl1dUCLH)sp=Zs34gv3$eZ2F(fSFx3c0G z@1sAs7hkfC`&NGEzC%1VN$t_d-Fy4t&j}A150oS-3HkwtAdxG|{s&l?ha8TqObitK zQx~KzH*N7i2PVTb3>1PXVGtJ?K1-e-*_OZVU=&(l*AlyUSEt?4XP-S!Uk4`GCw`p& z0a|A*mEscl!b2f;cR~npBd%QFs7%OENYv~&oyf>)$b7N0LNAGjEz>g<}XAW_g~=>}Jud>$rZ}n+Y)r zzVD+q8#o!jy;EhB zVu5G!MA7iYDOc&`<%N|cyHq&OQalQvSiz(<1`5o6Xraae$d}MMmJZCCDGJs482!joRA(PBC{8v1fI|($0*?1|?KFd{Ts0_zUui@dw~q^v2); z&Loytrcq-uxyeZow_QR+Ug&}BWK_EhDsA$65yGR13wkg8;k+K*zIk&fMyN2{HNfq zT!S(8CuIv;>kv9%VO2a%P5D#CIPbthLd&??*aqK|kSo{u6ZJ7_%mWGTc^735;mm?vj7|GSXa>SX<&wyzRk5JApZgY*!Rx@v# z=N^*30-8+-bNH;#cZqejBbGMcxw7|HN6TE@E4fQ+J_2qsG`UOnbm{Gk8zNnEn|cgWu%AWm2OlfLn|<=*bW0rkxf;nuZb3Hp|X zsY%VJyB!JTR7KaO657uV2XLwa+oT3s*A|X)!-4v@BJTaa6dc&*zp%d^M9CnVGx!OT zxyT?1gdSddQuBI~+{cV+*8+85`*x^8`D6^EM16ayz5{7SHnHQ!!A5k9)OqIRToWv} zPkvb*%#jbPyZQ_Q?V~?6-Ca{doA0hTF7@)8gf}}k74;Xb`6|33bJ zSDtGuiqc+{^n!!WBJ5*Zr3{V$y67=K(|SW)^q4=W`VifDHT548FN=R30p35*4F9PG9N+$~Vx^roz{bBx>OvsL zMsjC(Xu}b_)A<6Q=I5%N2>ob&WroGuE}y@z6l<~rp!~z2i`cSPdxdXs&;r3Lbte za^47)O6q>UA^sN9G6cyQAT?m+#uONs7waSy#Vq478SoX*AB%viNNgiU&N&)@e#{xc z^zJp=r1s7=gKXIE%ksfdoi8YP_Y*$sP;?Ol?WuHTDqpx>W&nyI6T=3G`xsLu7~kL} z$1bKYLt_L)4uE!$CB^&uU)*WMI=J2@zGt6z>#Fdth$MZ>MguYUg#Tz?oqvE1n`=b+ zdkE2ip!$zA;2zMG@uMW8NT9mVHKKXk&+&sX1V=EO?J9$QfN6bUfqf7u|8sg@3g8m? zpuiJ4PIniRFE;Q4xm19VKv)wh^gYnm#~k(oV^snJ*pVeg{E{3}HEDv-XOI1aNWHRI zAvOPdv3G`q{cSCMy^X+M4!IpL6~lQl=*w||yY&=DZkvIb77IV)GX9VvUKL433cU$0 zZ2N%RsqLmXw1yCN{fOTmBXfwgTxzT(P*830H9oTU-tMz83Liu-WTwTs(tN`t{K~7K z{l#@8)gL$bLKGk$+CR%JLD^6TWKCyqJ5CW!lM-r0q-4JuMVd(e(gb=wb;GdPQoM@yS5vl4R zVe$Vmf80>;BTLtVzl(NxWbqywqquYN$g%o63BUiS4Iy@=+RqTqD0}0KP>e3WdW#s; zcTBmq@33klQU7!&Bs6sfipYVH> zKJdi*ImOF`5bm`6&$!O;laERZblZJNzWLo4P7)$jqRRwO{lipn;J_F_l?9x4lL2(s zFqQbaWiaHNMH)2WgRh6SY67d>Nd?WAi`{*$=Bq1&D~t*21%lOwIae=4u5qYTVrl$#yyrcYpXSM<+U?-uE8PM zH6UMV$L?!(b-VvSL9$~o#^Egne-kRs{46X0L)tbr2)E2x6w#KkLSHt@?5k<6pv4oP zP*mc!H^siBdHZ)L3YBob_tXD~!{TO-63g_@3K-@bH2kPe+5ZFjuh*=fZuYrqIimo1p8re6b z8uAw4tzQPD>JRBVm8uBZ1HfVRSZ#r#s>&S$if_uUcor>oe+UF>SH6>2YcFnU4_H-R zUPZ2?ImI_=uAyP#eO~xV+Dmd-J8sA1dh#A2cWyN_>x##h;6mxc`kcPncssgns^c=| ziTEEgPY>p}xlA9O@tQ^*bYv{%urvP1+Z=U`{bU2f&Hk`uPjo14cut0egkOJiwCpoW z!szhD7MDrn98-b5CC7w?_AxpVV08EAOBUci%Ir%PaLPja1U+noEmiH&4`N>!k{WapzjTWc-LDO^Q73qlTm-;(V}Nx#?>s6N`E~; z&o8_Ap{V*ON?rP#9rxxwd~-X#xm)H*rL~UH^BLaUc#opRjEkh&XzPEbPtfFObl{B^O)KVG~ilWhEjT6G5mP5L?*8a!alIigot+ zSxr}ey5`06X&2AYW0Yggy+I#sZ$F0LO7iMOnM06NF(MT?a_PR&_lL2C_nEdGQkV{s z5;&p=1v>A7MBCe=?FSw>avmKz?m|f0d&8*@5PQKvj+QZkyty&d28QbS3WSbO2%&Hm zV;mGnJHsA2FVn_wp3IgC$RZBox`W~oUmxhlxxfGzPG_!N*{8lsRdW*JdnN4iuL>c6 zLR?Z;OEOnGf~osjjy}QY=!x&qSd?Voem6yX%oKPWBT=_Fu&_}1={n;N|Cb^rKRZxh z?eP`2vUQeb_XYT#QbnZj3cOgCEJuw6`@vum`I@=-QOB~mS<={>mYU{>>y~XaWKTN>T4R!Z$LePN zBm_dHR0P7R&+29{#+O1chafMs%Pv|FyJwBdy6n=GV}DwXjXXQIET@tH?Extm$p$sT3Yo2q-xo^K)VvSvi-Qj%aVYK zh255M)sYRp^qL?-+vw;Ic8Ak90?3km3!FUS%j0HUku<;v{eDx3&=n~wY|vK_*8|Oej9)>{{DnY_TFANUClJ_JxXWt<18gk5ew_vp8VO-y zMiBcGh6m&f+sQ^(DYXBk-uAX25V6n>ev`t3Blq5^Z;+d=xRtE)v267kdJ>n+{V zv*Xlg=;nOC+s5}$DBCbuhp)ri^Ro$2nWT2aMjAANI@~{4uZN-4EJB}nqgl)ToE0r# zwCHQF>B9v|ly&K9!$HK%zy4DLU{pe~K(x?(!-IJsTHgz#1{HEu*dR5~L=XZ zzPPtD4pjJCiqjUB|Lgkn*VR3UvosOJ4|#mEwvqOgO8I=j)zA!5i!SDW6lv4bXzM4` zkNo{$8WfM4>&0a+yZ=5^4mVfvuSC9!TUi~LWFSI{%3KY70pH2B!$HB&NAx6sWqn>d z_XXy#>=a&gUhs3$~o6yBvhD?s*v(N-rsN54)UR`GbJ$Ry(oc$=CbLJZU6shWbB8B>mW=B(%1+<(V z{A_!o-HyWXOkKe~r+)o+eNd=zdr!Ks)4?2vrT?s%kj|@ON4H0vWX9PYimOyyhPVr#d!m+!P0KVQz9pFd! zvv4?D3@;%J^f$qWlu8YD)r=Y%9TD3{CsY~MlUf$!j9v)rI5VQyN;MQ-zwm?HQGUg6 zkEH`XDJDS_wk zyoV&@LVb^stlBMQ(%F~og7E-WopS+!GFQK884Z}!kn6X4Nx$8x=M>MCv`wJtKjPV$5@w>@Vlg_`~#t*E8jTNcE+zz-oN1h>UaRtbf`Q>sD5-cT0k8GV5$Wy zb>s^!N3Ic_018L_h7(rDgEdQ!4*vZ%G!h~hd5-@vYGF9(H09|#KJ zbHumBMW${Z7uZ%90-K3JA$pD|aoBK^qhA!P6>d7=xPgB~LK`2B4<~h(YEW07l#d0&7IepoCfa$T477 z(WzLLVXzmNdji1t8zLCaJB7fuKS~rLBgn#-eP1yQe|$mlH4K^qOZiao_$mj5c1s1C znul~PVTyZ8;6S0OM?3r5M7?@T5md^-Qd_lgC{RSqQHE?^EQ&?7ls}iCfeL2?B}7l0 znF582y~1j>v%56+dEJtPukC+)ZNSSiP^NzvDrz6=Z{tv1`ni3$it%A)jka<4NZ|CC zsz%rw+09|*TSUp_txwQnXkVpppeM(sAo6${$tI+t04;QznPyUm@IzE^Ico^9t8pWJ zLt?JQw-Xb(eVd93f}`=by@|Hx0Qby@`W>OiS)!9~Y=lt1vOJ&;zht4XC46R`@NysY zzUY(o$$q6`@*LCh@pziAhrl(Yg3r0+qu%vS09ibBhcNEAt;8W1zFDcMYs9|8f->#=Ig3C4*9mN95+^K`eUn@d!&4X_f>~jJAXoXDpdI+XVfr zxP=rJC_w28ns&NjD#0~0uWQzp{Ul=f&PuxWIhze3!Vgazb&@ysMLuP#-%`|s>bug4 z&L?Zak-W~dhB>k4=cfYeV@Vmhwi(^8&eE2844-O`KYK}gWLQ2uC>>g|bDAEL6p8zq z5W`3XyrqV#h0|8yPlyb1qnhCAc!f+W1 z-uL~-G8WCJaSJ$Z7`?Vni!RJHjT?Bv6SUP#L~l2S^^tzlel%T}O=JyyeP(4^Z!?cE zh-ulCjyrlzyZg5J3lSO_)3W}nylzz^v|l?;5xI@G(xYUCk8OT2K#bhR{1qPxIgLPQ zHz5z!2mPw2M1bFYTD$K(E;2^5H&1b?98NV}J^HgdI&-AICS2z+_fk{%|_(x#KAu^;oOZESTzHWVO-v{1(XwwEIU-oo`M0;|Ex10a|FNOgL(c^RFP_ zU6~XCXZEb-muwy5cmfa3#6)I+?`PbudJK%Yg+@CsIky=G385Ozopp^fO=NZI5zMnAve5t zxmy&f!4b#IgRhc?-P=yo!{1fvG<_)4=APeNa2T#-SHVWnBnDL9cmW~ILk(+|P+bZO z^&=8W4`)|65Zoc_Zw=t9m!}Xh1zIW^8i{FA$%Z;tjF*?Xd^nLGG zusX=_Wqbc^I`?Y`>2i}5Uq`TngX-ag&DfxV9`}7q)~w2~B)@2T{7FH@TiH=yv0Xip zL*>>*a|z@fN-Xl!E4SfQr`AT|Ei75fGfT9BhR7HdOVUf6bVVmp(Pmr!xkQb~#Q8^( z@RrA*pFfTA$sGTv=ADyaIW-FQ<~48%X{l~fpPsVa+uPoL<=VQop)d>ak*@D!%S3fG zjKmu3Yk4oHy*0^w;dhqTx~u-}Z_!$NJpycFFe$=vag+5GcqcRL(|u%7!3@A}8`~v> z@&2ERP3Fu`O#>i{v(^ZEAlPs2uX1|QlJpHl7``&JxgW#u{QsnEPyE*t#kM+6tLWWb z>RN~bUwIf>Gb3Q@>PTBSTu*J+q7~C)dQ4lWrZnINOdMUtaEnSFrB6HMuopehj)?m}JRan?{5TTO+UC8@5^h3LBNoV`&>hf@FFFRW8ZMc;V zw%Qrq(zQ@j=yRY1*tM4gSMGsoN&WHfhCt~z+%d!cw}-jmrD0kcXcw|=HLj6J8}YNx zC=R#SFoXO*VJ4*bZ=M7inBK>6 z0B1rgV5@N-2S^?j2@pkB0C#g>8U84zk_tSb0a}>}GXZhPdHee0`=Gu8H+|$`eeQ-s zx-@LXF{2W}81gD0U{MJWR#^phbsITmL#_jIPN@TOj;(zAg9zrEjW!0{S0bbvW+l~; z6gG)kT=IN#__K^SJ6K<=KS}dv+vf)xY@;1!!#BxQE1!n;wxz@+^QxsJGkg-YI&@GQ z?==O?)wJI_wcT{zi;&!Vqr-QBasF&YK#E-`2?no2wG=FQCs|d5F-Hs_^=AJ{d-*3Mgoe z2Ka{PBh!!g(S9~bjQ)rcGDJ`$C}z)h%pXpB{i8`ykp3w7L6mKv_if2uBuLRH?vh$Y?T0>=n?<~%R;a|I#kNAUXMYb$kVUB-h1^B5{B2eb&9VF>{Aeq&%lR}tj9p` zeg~NpR3W@D4c9j&Ob(k-j6R=lf#C4BX9hvfyP#art7T*!amGPU(1W!JqGA!T7^M9+ z$&Nh``SD2(P$(okwP=jg!gfn*ELI|Qx^bR3YX@fqCd5QY6JT?O=JF7WUR}S67IXKD z{iJ%qFU|3GRM2!hK2YQ0m^3+;eXcUEK1XcFE1N!Bzg50+Xa6+{Y5SZKHJYD$a3q*0 zGr&!?Zo?7yEpEBr$Y7RNS}0LUFhCCaH73G*XR5xx(Z}tzAY3`M!pr%j*LpQd*?i}{ z2OUj4XL2<{DEd4FoJ>%eAdokzy+pH`gR>>~kLM$$)~P5sz0d+vipEob*vK@%*fC@L zqc6^BYlbmT)$d>Bvk))03`lWw1yi|oqF(70p!`+%k^=`(h4@-S^}g7Li-hiU(4_SF zq0y_rT!)|Zuumqr!ZI%AKo9)-L-oMRs~BK@Lks7Axo8@RxHbD(hSB^(%<wBj3wXgS{y%+b7+vSdXc+)dM$iy?&!Q5UE!#xxSH=3;mDi zl3qq%wY4JMPQl7*W2Zrf1xoi&Yl6jxgaUNCsc7L2Gev6aa%s*fw<9^;N1aY6!`9^z z>?nrRl=zz(otO@usSP{(q*YVG3NlHo(d153?qWZ=-?7~Wm24^Hoyi?&OO!f|+wUlx zycXhI&4pGNIVB)p6V!RMIgCO>Ka)&xYo10EO+ef7Bobv<>X6G}VhQzr5pA4hCoPP^kbw*DASGFF1{#L_ls6@nhILZi?(_1dl&w{- z^hdlU2I`M7PEi(%ujwAaUNWo(ym!|3T-1IT&Wr2T8t;`wNTc?#)Yuor6?E7Srf^eJ z=4vSgf2YcHYw>8yST@I=k~9s})e$r1V3*=0_QSV`j7kQKvr@dx&Ce6#cm*IdS4 zhR{$Bla0#MMd`%rhUFOAIt0V!e0q7fhGpkCLkBbL+E}x7X8XitqU87)4#oQVIKCZo zx0Wlh+&V`Pe;av7hSUVbtj0%E=r+iNPkx>_i)lErh9;lp7Q(Keg$eGtH={IoWdD{>Wys zJ+#)9f{HKSHIq|Z6rH${kvO!L1&g^f^9+bc$1FF<$_&1w=fKss#cu~vE^?H%^>ck> z?qhN6ZDbCuDc_$S)uG{yN$3&%y{&`kiyV;pn*$elupxesT1?fbIS>32W2&5)bohQD zMOZLKhP>x}Jz4ht=GWwm*elyurM9P?wbYJJL{VQ8CBh9EkoCWQl`U$fkFVQ$phdml z3m~Ewke2x5`oNgi8xxUO)5SZL03>jhK6;O3uAzKyEr zhlepsg6b@=VlVt0uw4gFmf*A-ab!<7Qp=ystO;lifl1|7>((nY2 zsqD@C^vpo{eos83w33Nt942I?ypcIboMxR+#*W3pz~!XRvbC(-V{lrr@&2qICow<5 zaLJg0QHF@!$}pGCT*Og$P=2aB`by(OHtRXm?tpG3M(+P1?yZ933bu4ju`OB5%*@PS zF*CEp%*-rVlEuu-tg@JynV}?$RbplspL6f&?uio}J@YVkBKAYYuBw$8m9-<*pKIm! z<2Q1{RZl9zh^`y&zH4{MjOPHjf;!_$B!(J1Poo>=1!ec5MKZlo{8Gn2fGF<)veAH+ z3%1vlVPzD_G7aS^sOzjqmFYh%!|B~r`gWZ&xB6NMq{WfKxlZr-X4@odrt;u5Lk{Pi zEK{)#w1~F;3jK!>u3Gc$&tFKhdP)fF{kN0nyl%M$_YNOx?X*|l{EPeov$h>*b`KqZ zt4_CZzwJI^j}MPfhYuapL#=Loc-3R3wlmWmZ(Yhtr?y!K|MA_O!$)1TV3S*yx*xZ% zHFi@C&${z}@8LCI*nfnJZN4!OINthre+|m&2_Vkup+K|uuSMY$iW~d=tc!$d|FJv} zn3XpI#ub29+Q`VHJNfw1LN9*lobh>)Wti0X+*Rb6WpeA|K$~OtzoK;w96F5gI(!5j zA3ok~1U5hGlI|TI?Uqhm>St-LzSTs6bM-XmarJ!EWNo)xXK)G;)EjaxChYK`bL5=G z%C*uoJd2(U87p&6wCbcX6i4|J4ef8R#oO7B7`m+`;aIJ&lXC55Pk#1;SsX^@j@%Pf z7tH(feX0y9(p>j)5=O1|3K9h9DQ8-sbHFe7sd)!Ih}^UeBGMD+Ff1I=sCxSr(!amo zV@+gANViWWby&DR$Q5kVV}>GRg{1_D8y{lp(RiAd=I;9(opNQyrugG1(7WeBX6}k8 zwCMO(^BMMOeWUk#ANDs>2Dr?Zf zKxy;F6yiNByyW+hMj~(;-!Go66{A{i?w<>eyE2@#@tu?)zfF+W{48E!*<9SXzz`#~ z?U{cbUFe2wUdPzU^q9+}xwuvmj0XHd0bKr_IE@7S7gf_!Zt_cAjAYH62NT%5Jl%G9 zAMOvUwa-1y8>T()S|@e5r-{H^r|)Z4z5PHWji%O|g?uj*3*S-r2U*TzQ0YgIIYN~Qa)(9XWu6kswx z23n@-RXiDB_>}x~tICNOv(%b%lxKj!Mw+ zM9 z9tyJb4^*Y>$c5p8@@m+xS8O(Wv(~ZhWir5Z(wvUu8NbUm2oTRHmbjGb$x`JIi;@B} zrC{@*@eR4PFkHEUU9W!Gi>RiuiDr$2@ z?CCXB?R-wu)5mRq4LQziRs8;a))vg`W4mdcEcB(JQxUq!WdeeYw4chGxOhBBqZ$o4bob=C4bX`2scv) z>_4#uu;qN@m{oc>{vDFXVcNB3en+kh)}?fzn0vvA(WUehx{^&lD{CmDmWCae!MGSW z?MgOAsQNgk^_=Zfhibd{(G>d+q^rF(P*J91P-rqiwk%NKGhf?^iDQR=HXARUB(3|Z0ojgHXwCE z4(K=QKjNG&EaiY&pAY3mgug9iD12N@yJn(W@J|1Yp^?kIAT56=hqa~-hUI!^*X=dM zAxwPIxeahM*Cll|zcns60*ax@Z;fZYC{geLlaLF7fKsMPps9jnCN>y)t0&wreA?h>q00SH*3QZ4fpd__)Vg#iq7-zVm|no=a`F7=8Z#1a>~RT$R{6+eInL7NCp|bef zYZ-EF*@H&j@-ds(c`~m3mrD{rV&;&f54&FJuk@%8QhGGqN^+2qe))@B*tYBK;tTyD z>2{JwrrOpYrWj{-q7Gea$}NePmNVSv@Xu2Gp$W@}zh}U@*|MRwaik(BYP#7VQMNNu zbA2tIk_Dcp*-dY9B3b0S*;vAdJi>G3iZ0>b%fVl!fBk-*b`|rpXbs)rTc}L;vv?xe zxXRh#Lixitkdx_aq4W3Sr6ft0a(s>jm++5pz834x-~22{`5ck9Gq8cXd<*eo9;GEz z+Zj@^d&hhWLdbsy&GxlumG~QlCF5^^FO=Ij{)Z6VZ2T~P-w(IC&WW_znDmhNSPskf z&Hbdb(AVxtB)2Gu)-Xa9!ayZXjy*Ab*b#OuUsv2J8V`D*Nd?H~{O$0>5s*}C9B^S6 z+ZhJ^2>dS75D(!i^QwB=$fqaqNut1&BDF`L*E%1(?8424R5GQ9(u9kQ{pniS0$iuQi{2bw)m zI$8ffQnG6#y~$OP|2)f>%)@F6Q(D;aMB~?aTrXP>6d0|@xr3bfI3nMQl*fyUUhsF9 zlc_jQiFrFHvS4Ajv>5gB#)`P3R(vF|W8zH^n+J|b_e{Z9P2KskqH}I@zm%{wYOoi` zRj*gpoEt{cneeqO%!DN-8IaD5u0w$hEpKWgSs|ERT0Z#RS1ZRCq>;TR4sZciZgjC9 zv?$M$MF3YNdtCb;9SuaZ;#l!J?tFRpS@_fBo@*&;7g_uV_Mn&)d>x%Po#6#>vK$ot zE;3>+)AQP*vfzD3+0p_^E-9cPUUOsbnvgN>D- zR$+s%cw6!>Kh^FzbRwNmG<5`um$0aGvlB1xvq^|3@O<$Om(SA>mrF(h#a@#Hce(X9d% zrj4i3`^u@%qa$nIZHefp?7}fjg%Bx#ET*Y<$=bRw)$8nsz*v=>GcDDrU-50espXQV z7u{-Ubk+cycA?s76&amcmg90X=-TX*J4Zy1F4wR$v9CydVr#SJJj*2KXWRw<=%NBE z_v{3Ow*c0+E90|`{iTq*W(0l$sFqSKdu8v5t|-$As~j@6(`mK?t8hK? zH2=oCQhg$p;X%G+pgwh~iLS_9G&Ds6se;3i))Gy|cH-Eu9fR>Fp0k+8jRD2{aEy4a zFt>`}@t{;^-;VnRJjTOZg{g2cmzUj)a(Hv%le)p3vocwo3O*VVM+lT>inR&21L!qZ zp_rigI+72~LCzN7c23><**=jH6>Z18@N}e9Hkp~+XvNB2lVp+c7-}XPHNISVo;Q)* zCTG@?TU?1aSM9Xv5jd@V7aF;UIp6rGU+a(<9b?s?UKM&h^dpRO4KCf=5m=PpN;|W=k-IpnO(%l7HYT1=`nDRDL*dS85#u_! z%V;J=)(sqg;!k`kDPBUAE_W<9_p7h7!bOkKuN?(So9PaOrIGNpUAkq5S4DxI-23q;xe3*s_}-ND-QQ+tEhMxBKM>hwLBzcg&HB1j zOgqys<@y%+H9xs{r7*FlECieIOT5;vhqsKTer4fh*^@1Z*Ir!9=Q6^#R^8rD$c}fa zO)wS5FN))VWm&!Xnt?(W7c)saEXw;0rJ@OS)DEjshxm$!d41CJNzK1+-7R+b=eth^ z+JnT}UQI$neXh=U1m|OZa7BQqJpv%tR!`2Q{gXJ)=^P>wIhX%r8TT7Ths7j$q;DQv z%-kx{u=TGeP)SO=AC!6M5iV$7d|;ylP;@_D6_a=_7d{U!FEHDmP~a7hWyDBUdnM%R zk4p#wBHc8l*_Wa&Kge+?$b~0bqgka1Rn_hc4qWFXqAx#iceNvQnOyis-ff2Qs|brqFO;>(xV*r z1NmGKUuzJ*PdVJe>3=;{8{6o__0)QvRg!*`PwWgaJtPD8!v5XK8ff<7 z;bxG=32h&Gb|64z?1Ld!O6RPsY^frM3#Nd4M?}>gJ-GdzOWC{?Y=hggCyzA-g_)g3 z*y5n|1wjGnTQP*sIP#)NwEvhgDtdHC-8?zYb7r;Kq;EBRB*J*mo4zN)MWy-|(m+m9 z{OtS#^_q~P7U#kk!&Kv(YOW{_sX7^HjJP*MW+h}oZN03`_ofSV!AX#_jT)UIh86Mv z<#;V47Y+rEKJL)I8LsE#lhm?Bm_i$`MUa1b>3h?h@^{o?wku#BChV zqn7#F!CQcF#-BnuZQFGSYk#dywtsiuBGXtAZG+P5fv-E>q9?nw_zYiW9Izd$B!^MU zJO(h7oT(X8_E`yN+8wOkt?8$p@qfh<&V8R3n{WHR-UHA|?ul0ZsD+D)L0sdoq_I(E zmnivZGy+GJ1}<5eVhTIs0Z`+n06Fo%y50OToLucuPwkHN?2Dp8EQCff1e$CR;c%yB8^A`o zyW$+UDh0cHeIXU&9JGksT*KVxgcHORKUzN`J%S~bxR1MryZo6G9zsd38~m$KK?tEM z$xqA!h`<5U{rx0l+IpaqfR@Li&%L}8!;Md>u#Q||J+m}|$4^VA&wd@zs7SE=`jY0Z zIim}U(z8=-k%{f~{Z$!6!@2&M$O{0$mNHUmg*HHgHsss|DXQr0N{lV3& zewJExJ4W?=CYdR1i=mdfcjw&`5M%=RPz}1{dOT$lONaB&u(>9mTeuQ;of!THY?%rU zUhI%QfjA=Trm6{ax5zG#rxqrgJ~6x()xI!Xhb1?KWhMz90xx}Iy#-EMpX+LGs_YPqB6e~ zHIum4+8orqbUNufiV5ir{#uw-kF61}ZLgHlhs(xn3OcHn?xf@kzVYx^dn;y-=#$Jx z0xyA`?pt){$%hslzATb;blu?mDOPFBc~uAks~N`xWSL zg%`0P!0rA@AxpdedLD0Ez>vT*D4t=jM91M$XzO}%OVIbvoQ=rTTI_t+Iur-|p|au2 zGEvjEVONsiZBs|GTYY=eZ%EA=Hn!z<-$!VI1ixI5;gOMnorjGMyuYVQU#Yr>vfTao zcVTPuXW?T_*; zoBVzOV)&>#8B|f(n~sIUqy%03zAq26p2hdFq5a)BZ9nozFP~ZGazp%H-+)WEbGh7l zzzJ(v=1->IZ8gU%%l$Z~62Z=vv~8|4Yl0m~3g`WbUn=JKHkpuSxD+ql8ev2Y{_@%m z-vM~1dka%otY`ac<*#JJGagUC62z5wnSe)7Mbka#L-{!@!xnWWti-rc-J+lIQU2xQ za17Hjtcwq2@Xu3Lga;9k!vRrY2(*BfXLXW;gBr0K{*Z^HO~=F_3uJq|=ZHo8J(VCq zl6qXKGTufh7o+UDm{(||uTx|~@%#&u*+@qN!e^cZ20f@;97hYZBjLPkvc}K4ete@h z{8i4QGO3<40_=I`;o?F{3*Ww4cq}9eW$bS;FrcS>xeK3K;arvg#OM0uYG7e$8&-DH|t@uR679xA1S~=%m=4(=hw5o01 z#Y6C0tzH3xzb`j--t`}uMoBRbY=*AZQctIh94GrN|vKl z5%uCpJmLPZ5LGF z^6`4U^L%B4A@th*sBj!v4=HID0y|*u;s?C@qE1Bxe0h+@8n#?6f=4^KLTbMsx%u?G zb@g0ylQ$^u`o??DKeU;omQN*`zEUI*JA@?6(=-WnAcp)UX{Da00A({=tlF%MLly3p zHC>(%_gjn6y6^=}U+f;R#g1k3r!|^#@%Z;s(yc#dtLl|dd!Lo;@P1mCgiU6C8RG$I^`WREmgVIx7c)W8PeoUS3?B!&em_U=fD=M}lpFs)6vPMT9MmI+L zK8g4{oY@J%H=G`5HFXunUu8SLP76*~+2dWYuW16UL?t%3Xr{8Q|N5Eq(;_Ecl*}wuOGG@E|P7B>w7x9yXOV?hZ}8J z$R3?+1ULPe)FgXYW&t0sb)Sy(7=%bIgMWKAjNRsR9l5L;>_e(6hfa8QdITcxO>{ho zE@}yD&*S69?YXXh!Cg2A`C8D!^}in;8c=~ZIVZ@q%m00P3O(fY-yBxl=~2kG8l=ZG zwuoZk&RjU8&VUOk_Q=eNG#t-Ro9^3TFNkN#UIZgp<*yh7X+&#o8os{7kXVY-3JT-R zFCQOsqq2ODA#+Y|Z@EvK*XceEQML!m>z_;EIha*EDW?1ls?OlON1909=(^61Z97H#ehaLla1^q~ugHYHLHW4Q!YX)pen1b)XT zeRX|t_c;D=q+umf54U&Udv$dOu0CtN_o7dvqYg!nJdGf);ZK6;Dk8#*EkCK9KFaOG zzLOnKtUsDo&2WF5IwLi;6YXx62A=KifCHBv!tIg4{1GjL<1Iq(uW6tBa*9QsNgHK7 zzRXPFS^Ps^gcJiTvbe{eF>aOHBLSP;w~u+BW`d%ynogLYu02qTTk>I*Te7Wt2PkP~ z0bq(o?w(Ls*Kzs-DwqxG8IdUMevY2NwlS1~Tf=WDc45!4%(QM@RCD>fNSl0`(`qmp zzaw|LXX@$vT5S$6NqDXjz%o-PZ@pcGyDs!QZrqrD@}iHjVOwx{p(sZXmB9Ihxo`+Z zrOr3PBAh||H-u(pcM&AJxS|S_#4vns7nl**0dBuTyN7D13$ORgz9K$JzVDK^ z3eD1Ie6NKJc_+^Nz%0Aqsc9KUOKbNPIUje!dca%~m7{@5xUYa-Wu;ij$TOw15-9&b zdU^Q`^(>htqViL2{(LZU&2KQ0$1MB)7bvIizvCo+wVF=lQ}_7j4!oX$9lfSkfqUJ>*4@QuSb$m4)lv( zNfi710>P^q#NYIc9iSqUWojj1cUDDWLhx6M{hMIBMw!=pVn7PIY>|Iwk9iujGGW2Gafb;eEGZP=&K@rHVRAKZm>mBOEd9a>Wwi!Dfp%7?b) z8-@<10F~kWi6cX1xT-=ecJGysCRrc^3=DC|^39f0-RW^4(34ahuXvN;H`B+rX<_Hc zxee=n=}|^%ymakgJ*m)YC+cvxJ~lA6KzDS{s9;sa`jDv5eK5f|U;*1187C{_P-?ci zhJ_ULX=5Gigz9T^L`Z&$aY^nwn*eIXlW7KowVWy5jr^D?Qkik_D%$huPHw00!Sb@# zT|Y=5;!KAf?2MG$vV4^#agi0(75+olWF?*O_gDXgADL zmn(z$Ah(j$3?NC;?I|acxYRDKRB(|jNY{Afv@WVB&bVaCDQu_f6ozgQJ? zx7`oVBd&y7K717R6IPfYqkAU zVh#V*){4VOUnwe4o*!o(>C_2(ENv4G{vWJ-DUJ}GT~ykICAZ$A5QOHXgo2aPdo+>W z%c|@H(Xt{$P>3f8`-!BWZr<+_1izN|w5*`LbV2{sLCW#K*)aY5>-HCC)H0@$)#>Dqt^6oWLghe2`{b=#a+uX zniwoGNT3Q`aM>Tn4H+!V15k0I7jjx2}FPoN;a-CmNb=yBKG{B$_XT(%s z2W50I{iHX@HQZ^_Su6T_6G}%boq(%%OtAFR`jItlmL1&t7sFdb?{Yte3v(X{q`z>p zP+(oSkhB=BEH-m1g8$w^JAr-ejwLIws_Z{uO<}`#&^6;{CpgKlQuXV#HkP2QNeV+d z)yJoBYB#ME+xHGQ1n9*IzfW*B#=!k}F(xV90MrSnH(wb$^>4823S(iB#R9z3qk>;@ z#zhp%Di1`o-Zn&qy?hi7Nj-~se<#1XGUj+wg&Z)cU}7ocz8+u5FphkLOXM9Vy7|D7 zSvg|o6?9fsnHWBQ6O3r1)8q)Vn94}q^k#X7A+Sm_ker*AK*$6^k5Fo&<{C{1qTEc{ zrKdp;8XQcn5!#lqXl_b~nxl8p+S<;^85PlaNOwlnv9EFkW2w8O0~(%Yt~M2bvzbcl zEPkWEK!FSsVjQU&1!v7hbG00~wPftk^Y4n6{)c~>!S90w;@v~Ir2205?#Of6;!Cf; zX@01H+5V_NF{=clAdU??1;5wZz(*|`FH%&c>Z)7IN)!w7M_TGL(RC4#61mr15?nr> zmf~*<(-X?YCN7S=`I@_}S^@FF8(I2(Uvz59H4nupdZX!?1YsmJASJRmILPcyc|U@< zAl#)o%a# zRUO~9eVHvxKxr^)96_GJmw$gCPAI*J3_~Pyp+^qI0ZGV|gS=U4wlO0GJ>GrFB4SGI z!u6-GF2`3L3;30w?rI%~6h{8yF!n}#-lu-x;!vYlO988Lp%q_XdWLE5;`Gu*#l>6%GHAyeK!F_ULZ4{RK)!UQBkY z7qp1X)HMbmtkRTsE_s&MV~7I~RTf2`sKAlaJPKBV4Y6G<$-*=PoWo*`7PAblv%B(X z@kj&&pi1VAKO0)=VO~tO)56#f**kQ3fWiy5xG+3iM5?}n3M%N=z+HII22dibtvAf$ zO>_$5D=6W3U(UMHGWP7skq>g}R=E*O6s{CHtJ;Tf9z3&E5nhD3j@Q_bW|N|Kyp2d} zN7T7QBu~XeB=@$A*>@qBdndH>*7y#!@#qmYoxwlG2?%P`27O6g!a-IA|r(FL4w4o3XiBr3Wu3ppok3GYK?T{EMza%7)wVO(sAS=;t{kN390$>IAnGsZ``nNR zv$v@AWGoE9O*YS;bN;Cno6Aqybq0*1;ix#U4d z|MD`^V^F{zJ{1^-Nlo8sg;RM=q#O_U^}f{g5DNE!R#ldRJ0Y60<-$@mZzfQ?ttQVi zYX!i8-iy{zD0>7*h&cfGZ+{I`~Z{J72(7e{b05=oRrzZB518yLYygH3_dBE0z zXmw>*a~LvC;Rzn;lP^}}nxy{{wpHfdEUHLFH4>2Amz|TLKEJ=l>!ht9PY)d^CNj<; zV6tyk*BG`!J{3kxy%Yve>dMq1FLyas#gE|q4zv$JYYXIPyB$5(zVyQm%lc*ldsX#K zuH3cBNreAhr z#G`a*;g9P1tf~vIvTo-me-Y;KBA{cW9D+7(GFfvYLyaxV!kcoGOP%aNsZ!g3sV~=N z6sO1gcIf(V1DE!6aWPkidmv^mA+lC>W#MmCC6r+grr-$mf+OcfjlR!|&pH~KMU7^p zaM9BethO{S^N^P?JnwMsYiDpn9Jh2w92brKHk=|!n?ry4u$1nE5kGv`CRMWz_cp=| zzrDHmDatKb4C_{)cVt$#No5vMIh}&u4A!N9JdkqPRTINK?L?`kAlmK4e`TWGK#Bx( z+jQHdi$WGx`g0AL4~6YnBA&K+v1Hh(Jd@0?j@_;Bu0}Uwq}HlcpAiV6q8~+_HpM5Y z*cCJAj-zGBAs>%oEA#%Wf4RR{>F5D7II=4LH>LSc`SlN_$;Qsi`Y)l$`hU`tArdzVtXcs6K?dr*?$F4{oM`k_Vlkb zwSu;A!i+O|urDnU)$duGn9jX4-Vna7g^&y!u}k zX&U1elU8M~=_)2=s*}^4nVRMqw-dmiTDo^O{Rrbv|Nt;kLJjGSg(>IT?ceL*t zS%`)kj&woq9W}@N$ynQ8f%@C^jj*S?>4n6wGTxi#jmm}%cRH~$?OrfEDCWKrY9^V?ysP+d7 zTxFe<5`WPQTQqpGl8`5txG6!kqB|p`WMD!xC0Vpsq6Q|hn13Q`6*tJ&TQig&kTXoZfm~*{!2WP2o4N2N#V_?c3gaHoco!}9RFp%BJ}__OU6{yh@ySP53rQZ ziPS3sif@zqlU`yZ<+#3=fOztNU{1GlpHl9jZa1IuXX{0qLL8phmO%fcI~{eNeg$J! zFWpO+*%qhGH~gq+cM4Ge`U(djEO?T1Uh$*re9(SrGKTU~Sy}Z4&~;cn=p1v!8<(FM z6*@BNA1^WRCWT|~QRo+)BDqSg^Tuh?7M5>VA6n%N{9QbT9cSMHQzk8#^wF_w?;J1VPApXJC41ZY{Nb>vr#WGh0txNYZf#2! z=Eyf!o<)dJ^dh2ZOXfiB%pCYS zR}11a8SojawoqgNo3B3T=^nZb)fe>Y9AteGPj#2yUAVNe_u+pqZI~BL#|n*a;L^Gb z%N`cwNj-NmU@MIOslXe8vj!(sIzf+8g&C)0$j)u9WRz?QpwUSRh9-jH9*Fh)Hunq6 zI&qhcn$E1a5+mdxJViA1h;x~4p;a0u$$Zml7Ngw;DIul)Tn?>0n>fGk0WnPJYBr7c zju|T5=&I&}lNfVOt!l0hh$x((Nz!{cq+?s(;*1J-I<(0_Pc=e-Y3#L*I*Nb_C~R`c ztG$5SA`A^DsW~+d!K>LR48L*6R~@r2kt`rLwJM6>b0k&Liy&hVYx}r(HgPlWhv~9- zmnp0FlA-aXg@Q<;Nk-Hvtay>?XU&BxXSJppZ1@@UVXF_iw^6ARSv?pau5#U^F0vU)S(Z;r#d7I7OFx;TC-oy~o#9jvv zrQoTsX5jQauYw9kFpK5N zwz-hOXF)mAh+Qpb?!%j)k1h8PQW=JP3)w)yin>>Wx!vZX{kKWs;ODXOYH?_uwb(zm zyZTIr`rA<|97W{DT8qCnUL-p3T%^t;vT@4KLTgR))zygqxFZl5O+VPds5~Lo_6Xnd z;2v#8>pS!#D4UV5Tx~JqEfc3Xh`f3nwRj z>9jlWEk3j_lJUt2L)oWIc>{lG4aW@2x0U{M5SC{hv`~{_L@0-fj$C9|rH0XBSTrxJ z;Fv#bD??3)PmLl2+-$9U%OGbE^KP$P7#zo;;Yj|~3|63e?Hf>P_l(%VDY~Ok25qwu z{sr;T>}e(`3!%DS6+BSm{*qGA0~P{__?G{CmK8VYdxurzeEL}iCWx>oRZiiTJUe3r2!{dj4+PwnHFqnZkUa@Kl zSFvE!Km1we>Xw>{&8+wQ^c_M(y+(Pwa_QOlI|kY}`zs__5)FbvLc2l~PEi<57(3SR zT6jCPN5YUO5%4??%&>)3dnqNaH50U`jIGy)zb-7heYjE&D}jYKMyY@>IuH6QW|0ob zE)s%0aG*4^)duVXvEUBg%p5?#N;Zwg_TFe7c|rPI6^c{U+m0#b?`b|d0-H1ra5lt5 z_Ug^eM?V06wNDI+Gt3y`(pu?YY)W|;9b~lzy?B^gP6=5T0tJq;nxsp2O5=btyL2Os z57If6FKjpCq0_Vh>QC+5rSs*kLpH(NJ+e(|p2-89AD3}BM?~0S7?E5S{OFqAw?|T4 z707Uq=6i&exGyNtaW&o}DCu$EQE0xv2dO^ov(?F*SfYkmd-b}X9Yh%MExKR9F=v|g z@a%OXezL*4{&r@04QymWVU6~zr{GE-%bFteOIYRBgte7Z1D98M79Spq>BQMl6{Irx z{p`^g?7%n~GxOuiFbf>Ddz07_b)X`P(;Z3+m$E%(+E4LY}hG?ee+Q zbhG=v9YVtX8l{qM+Q1i;-F6{PG*>&)0Mo3MG;Udn`&nq))H1A}~_r9fCf=#R%0^M*b~cPL5`>oN6;>#%p`ogc{pX3cL(q^>n_T8oobp84~*4 zchLOI-nlIL(VD=60g)tCjE&PZjciN1iU*L)R3ld~Iwr;G`P?I7?gH4Ro;(G-jO7Ww z-p+N+%$#-v@VuNv_iVl6QFHONdq}l`Nx)M_D}AWaLvKi7qOqarGc0yYg(;Z5vmvh} z6;XVxq7AIK?;dh^zVJX4USh$&Y4nw|6D~Bm)2yk#8hPIMs#x_rn+l={Zs>vEd6L#xBS61Vm~soTaViUdLtKx*2T>4* z8q~ilnE$Ai@$m5chl2V4rBudE%=$0a(f>x({Li+e|5P*oRo-y2{O?-efBIc>{?jG( zfAS(!kDajZ{|+zyc#B~$4^27E-z6swrMw{({~YvFQ#DhH`aQr+4#N5%ti=`$m-#fo@7sML%Jd_qSqck_*w0@H*y8T_vTX)rA7d1W^uGxv^0DF-Q`2%BUt&Ch^MabWyuiFb{H2tI;} zy3R*@^kz)!HM%I=&$TqBIh@{PwDZaMsUb?)TOIGgC7IykVfR(NcFbm_JH6o->X-h6 z(Fcaat=%=OAJE*ENhNkV>SMHS6fuFc@;?A>lzk!WY+Q(>7Vzm6 zg!S7R=`aeNZRk4(iA-N0K}w4hwD*fL-P_X`SMk1RKbp4BqZ$>`P?wbQao5g5UV=#X za8=`0jlQeTp=phZtF2ed8B#BdyS^khwUWf9{F-fqSvk}q_jhr~xcJc$nu+>tSyZFW z2_UUsxQPNi#c|!dMoE)U%3K&QCh`j#%f|=4@p{mB|6k7tDE}A)@Q42x$ zu?x;dYP(9$^_x|EjwE`HFsr&idb@qqA@ys>HM#cUt0^EsEnknht>(w#{4c)}EvR4j zxE+s&f-*I~?UF18>5U1ZDZ|Yk0-MZ#zmPC+aFKR2!fjgdN_8n%%U2(6jrawbMsSn7 znV1Gh(;4Kyyz`COZ3FuA@-DtH3khnts8c|D{!(hF16xM9 z@KG{PiRDt~v<(mZRZ3{$);(S23n=c?1ky92Nq;EDd93#T8qm6mEsr-UAPbd{Jv31# zst1y1FSoNinr*Jm+~9iCSW}{aV!c_0OJlIg#ky0ziig0V*?)x;g~au2>eKAXToFHL zaRXyvdq0tEmh-`@GR`X8&&)7NqMZ#E{;ApT6xSp?6?q>8@$La&bxN_1W$^2Ho- zPg`8$-!Q#rLsXo#dP-=Z2CVy%?R@)Vq+w^4ITAW%*+WCOsK z67$~EnQ=^NDgipXvKr~UHIROwmIX%h@n|MSgZ0^5+Eb;JTi420#1>vzNfL%ke7#D?8OiH4L*2F)^D8L;p|z(kKNF8E~jR6krEiWcjLkf6;D)<6QyJE0uG=+W|V)9GsnOPhh~R(}H}-Dh<9A+IRyQ zwRbJyl6qi^ypT57o`tNm)fyS=%%`W0oy0_k_py(7Ob+2T)89UNjqe+*8wm6jB(WBl z6!b3^qI(FOuW>#M{ont@&)pT>ZX8?lwm(P4{oI8tEVqjgSMn`*iuKTM<*vuZpm{;+ zD;=M8+o`?F>h*-iazYY#gt*qfq|X&Sg|k~Jp%%7!&MiCIm=Y9#Hk=l?e*Eo1L`)3{LJ(~SXzY`G0xv49TO)U`2H>AH{?kjhR-Fo*illX^_? zF`*+u`bmYmG<9qU@ngRB-_wbI<&7-dTx|bJCOQAJYw7=B-_`$|O#b)z#y@+i;QrIw zp7UQh@_)%8|Cy=&PdQ|hj;_lFC*~HY$RGMObyAGk>C59(n_SUaO-sengAwpcW}#g( zDkxEnZOgEy6%w@~nrhdTSDG04$#J@mhe(hZnpt`&;9an_|e`)Lip|#Rj(VzU7`x21l*h^{I+gVn7Os-cIsD1%$!* z;}1Jyx4!JBen6ovUHh1HPCmYtaG7gJzy)%%r55J`(#yP zxyHdsPP086vnx@Y@X5G_A|em+CP!+Gy~td?SJ%89PT;}g#YmKkc`{v(N+F^r}qfHSTI9xt&Y$XB%JAhI-AfE;MG`XTm0-gV39?^yqDpHg3e$2SiyFSbYW_WbPn9FHh}Bc+5C1i> z)bWoOHVz)7AZpA!k^Yy^_&dhh*`BkY4xS3T=Hes>Kc6(@aaWke@232W8y$!38;jh?osZb^+3(8OSwHG>HE*`7Tu)CD;mFZ+@P@A?$&_AS2xFBY?kz z8;B&3W%G>u$zN|$u-=e7mXR*f;#n`vgjZl&aIWWHd{TCRz`twb86=Pg#=BMmLN|6!EU^(%(B>~#e)Ui-uy9^F3SXH&YgfT4H;bAG;^#6;w zcl^?POP58{c4b!Dwr$(aO53(8ZQHhO+qP|^(zkl;vATQjb@u4K_l$k+`3L5QH|H-h zo_Hc+PStd|B&FSbbU8%S!c3;LT;2;sw`)5!F=GK<-S5f%Hc`jH%Jbz0I4OU$&`=lV z(Dhtzngd&uohd6HbiNDFCi^3|PhN*Y%7|X3>>+ZB5W28<11;aZD_q=5Vm;oyGw^Jr zgppX0_Rp`&F!aT9F&axkNSIHEJfSGy%4$HCYBC!n8%Qz;J0#W-IhG{RLH>}a-J=0? zA4qbB9+2_v%A&@Xi5;_urx7{9`c`S2L5&>|n^0exk`aj*H@Yb+Gedm+D?OF?L*7Z^8DxA7Ok#pJ>cQhfAlsYp2(pQ`;)4#A_cJ~(*PZ47!*O7tj2o-Ml zvkI$#w~*#HF9NvwuWHM57ZpDUDshszwW8Y?aojX#*FGy2nQ6%20+l2yAFAUw`@jc* z6dps9Ou3vq$fg9-alJ|%wjjAmY3`T`6+hqh(XT6MNSxjkg^P;g%m-=@n!7e@_j8ms zSx86D@Sdns;qtn24Pdl*o7kqSEubH!{u?!(3{xAi+9FVwg7w<0Diyw47doBTpmGHy z2ngerwi$KG;opN9%qwS_lSoD!qq##hPMO?s(Ljm?orq%6$fV{*pj_gt6RganaRY`R zg3x{IBsj=W!HGL)sbERe z`txM65l!!92+t69Puf|v!YRQ81CCaTYCIW4^cSmsvw~H$d;Kk;?q)dAOx3w$si- zC8H@uD8h@N%}^3D$&g%iMXTY>t(PCOtti*38T^HM@R~k{H*lb0*$#;VWK72s7*sUq zZJ~9}L}TF-KvAr?ju&|0BXcG9?u=UgCj~?&F5rSL~1GJGV;Bfa3 z207{1cUg}UXe*^w0 zIov%gYEXVvl8W}zE_=k~akW;wPmR-BjFd~8W!f$|^ziTbMJeqh74oF*P@=X`i)I;e z_-1fhv%Yt4LvXkg^m)>!zf#Asv9eBn9cww3yum(gp-HmWJ@*-Hrj&o&({SyGAGx7U z>HoIW-1DPh#+KA9Fm9;6wwLWVpf(Y{5P3=SKw5Ah{5T4oUUbUqwvC28KBJ|m%mZPt zgbUhEM|X86!WE#nNVK-1m46nmcGSd!bg|jlT)*vCgNSI|9Vi1#PijLi(R@)MAM5PW zVe&{uY{Yf#SU!O1b#+p&s&Ee5Z?^L;o)dI8M=vrg$^*-Oow8VH^Uoz5a zpq+MT4^MFIegmu4WM}69aTj_Ug#5*@`l#H=To4~aw(M0|ffX90et!aa>ffcUy`Zdf zcwTnDwbIQrv!2{G@OwFvzj`?ggh+p%@KC|0bSi~lW&Qn9@t=W!-*V^wzT)`@IsIpKjP)P( zlRwol*1u8kf2oc&s;yhEiy&Oqclkv_>Q+AwX<5G-4hv8f*quw5;imD>Asf{fiU}8( zpImc#;3qfHrcwo!C8NgFW&|QiRlXi z3119YDGq>v2w>m)2}7*VtN$T`cjtXO9bk`4QMl7Jc(-}s-ISvBmA9Yayyvp~+HX3U{URrI(59vF}5RQZjfE%fL&`U-a~xerG9ELFMI=_E=NU#bkrpomFG)G zfDj2ZiYCG}SGqMpqFPVO-?>?+>H~Ig2KTxB{zQ)#ziM?eZ^)6()$tQX8pK}}2xrV! zw#Pw-z+VM_Dy?EH5fTx3l;punr&t-hEy%+5+jjvdA@Nc~NfFSnA$}S9dC}$(R16tY zItgg|ZgFX4qhfcT4?X+dkXRDr@$BgYt65&0nK~(W8MEdyRby2C3$QMh1E`46X5Um7>pf;j<*bvz zEMjP~RY~=Y?uWC`w^;LRXBwn7t<9BSCT--dCU_quEu4%jlydua7A~Nbn?_8D zfK_?XN3pDIg_iXsg0ry3xw5U(8mPYkx@o1t4U6&39XFZH)y!TAw!WlDF7?{ zyp~1#k19sasHW=6h)x^+v*&iyNoQi`)I zMCQ~f;yNm*pyd#Forj}9roxzJ%{p(i#r0g+u!dtq?GI>wBCIKj^rzGE6&qB-%J_3X z)NZ;g7-qb^TFR_km*VJgVXS%!LeVm9^?4v?*|9Xex%26uU&?ps;bqqAM3J4D=W>!eTt**`cu^W#iZWygxt2dC+BFD{jaSrmWGoUYf#r zhw(qg;>DS!n6iKC^~fTsS~117ffYw|@gY5>4+Y+3msAKZrWqf1(!7YCAh4vYxoovM z#H0e5I_fMwHo84|V%=D^bXIPAf9eXRx`+5(Mup8m<-SnPVYV=fFCShWc|o>1t1=+TRAAL z!;I`cCC1644wjEulf%#Nx5LttUw_B|q;_?7X;4iyCXj`%jA?6AHw@$nlOLjwFd}*q zmhet*6QKELBFX?v&ZFz$O*dj2AtD%H`bW4$)23-O8O$yjXt6xXML8{Nc)aJb7c=$4 z&{Z+}*if4J9RVQOSZ|=A@A=}A;`|g>Qsqz=QlMXeDp{u@5Zr>q$kiIG7VW za~@_Q%IN+UP0R&ZeOV$okxFRY8LDp7PhauH4!}ilvkDs&%HR`^Lm5D9u$Y2x&w@5APT2+vmTw0v~+gw?6nam=S zUZ2^%DO7MELUQ%Jso}HS%Qj z;@4n0D!D}1_(SnQnV5BV{-#qoCAu`b;-a5Z_om}c$jFsHAp_IW+Xq=N#NYmH5f0|s zC-6sSoNbfxXO*^vm zfXQ;~)AV9|@#1(67K3AQ@sWH1Daa8<)^C%>4Nke(A<}ENru-CMw%1#Z!TSuhs=x1d ze$M$R`-0PhQf+lJHR|9h^;{0As3?`WuK*s`T5Cw=N1>h!Y`XPw#fFw|z1h>V2kW8# zv6)t2V9Du&Du8}4CBaaAT>o(GdK)n?spxejE=FNPY~eb}0QJJO^=zqyNUiK%Axou* zQ%XR@S|C%4mT2*znv)e}xqQ`xwn32C^XRvntKb^<>nW)wSlyrRLReh1L=u6=#AM6H zggi+mOqVxKR@LB{TxWibiW+vi*N|rP5Jv3bM94RmZ7QFY7*6mlLIDk;Lt+JK>5gx2 zd1VV_%rm+RiP4&*`$Nh$o>^F1F7P3ZE+w3TWXy?j@KIpHW$uVg-m6SkY|#7zScG;x z^eICFR(GK0D&xz41-d_(Df>U++&`9{|L1V--(aZ!t&Gb0$4d5}WK`BaN%%kE+#h+^ ze~EKHPUF^x5M{`}p2*c&sfd9B*ll07&uSV_B$FD>iIi8Bpd{hM*@(mdb5zn%N6(F~ z%n-n9X=*g98iRYJrk6K$a2X*ynV<)FU#~qqZV&gyVMgiiO}a8WJ6c_D+4q3GF$zqb z>pS?)D#^0&d8E8J!uwZbO<329W23V+-|c^v;ue{WWw)zyb-vmkg(ZgrY7Ta524CkufrX|`x%|tKssNQzxM(;B%5>|9j+;L2^xFs&WRLPAq z=iCbHi4q--@6Nyz-Yy;1sVLsZBBvfou$Z)B!TKWT-0KsAv&%TwXft-!Hsl75kUF=i zre~l$ir>bKz>8s&xinRj)uDJ)p?u5F2Ymq=p(xNV1OXABQ3MXb63`!iYSot?r5@m^ zV>H&n4DsSY3?A$GLfIKVM6DhWx{&YR4rL8Rsv?MDCBU%hq}H*aY-HQ2a3Dqp@@HPR zEK9PlEsf_?QmfYEE!SC#!)HtdcsNXy99Xr&0BjF2*X`C*EKK zH^UT@e!%Jbt#afP5E1HH;<^+Ne@sKT!9+&N6c;e3$_=d9A{)~0MibWQ`m|AYQ>v#J zG*~lGJv#B_0ce$v;;qYWRNju(RIfd((0R6}T0{Kk*U%h3f)ZjV|AlBI{sP)+8g0$a z?8U>Moxk2-I$ejqyk2e>VYDgg|5o+9ABCNyrSsO%b?ZiQ873u#R~mnMA>RgnImTC-Z)huzvd` zRPWQoUgPs<69|I;eJ9(2GQfax#@WMdvdk^@8g@O40Rah|06BA(59A!G_XeUco-CtB z(~hCQiEw(F3?sMQjEDm&p3e;+$LLfo))aa^BP7(fxHlvOlZ4w?=-4=m_nbP7f#& znT2xvLJwqPK7<>hIZ*>hW3iBCNJi+9PNJwL;HreC{5(cMd3UQ?z0{(&2*~wzXKu@2 zj;#tXwuD#e2(Dg`3cc1Kn2krro$Nj-MwKq?omA2?@<4H*a%+IaFE4#S1)MB%(Z22N zQJuO-9Ix{!;;jcCg&*0nfVBLw9*+D>z0#kOF-|B%qy+0$=`f>~zz>Mrk2AN!jOgr$ zrnD16%yBr}&Ze4x5uXQAdbFItK~+p7V+A27bjB>jEG@_+k`T){%|1VRsE;dF$e)fg z^t*bH5}xRa?5Sx&ajYqt5oK)3dipg>yT2iLS_n^x9ylmhUfnJ? zgy#m`)vlk)F3ugyX$EtAPv#vm8N2$7#!fM!8Ja6)9DzaVqj|b9v`xC#`BC&rv@U_H&u>kQ(Sksy4y4vN#2Dh1FIeMOvd~xt#7l}+B zo`xV<^xOA@3c1>7IPBBae)tleM2clzO=?(;q3&ab1Tv9Rg*c0(1Pe-}%T?@{{{2^q zldEJ1@aM>dYxQoQLQd+x2Ck%*u;b@yNoT9r3*qw#m)B|QUgeB2wW{0p?>>PIdGvHu zRnlXn36Xv-Uz+iDNYJy9LZv&5k<*U^U*AXa^uQU|`cl8iS2zK9mn^h0qV`Zbld)8k zZMgZ&1u%h(nQyS#<|JK=|5QHf$qZ?Se{%mRkkm%x5ZjDtBB_$5{!QM1xDuO?%-V>i zKuEW+3!1#9+GFx8x^Klh<_H*N$;4ALYawg;ToMb)ioh9kw(IbTh4qsBld|^Oj^4}h zZQ?p^no-t$4=QLvNxRP&5)|{=D#?~SbCD8yzq^U%-1gYkU{KGacsYcRQLdS}_GC7t zsi;*v){hcmDF&iOxT)dJC)Qv|VOI>l7hUoE;U2l@@s?1W48HEz&nT z;p|m<9}dA*?D^zLZgH&;BD>LzREQG=aJfi@c-(w_Ln4H6v|=W{T~Z!{!1XwSGdd50 zCapbC@%q94O+-PSFZ_IE+EX~E_Tf%yPU@&r1`)>3wS)!7&C z2ZS>;*XcODNd@DgQYkA*7C!3dUr1+q?8%8SHx1#PESMp{^kR>6>@d~SrnYh=LCG@5 zHqCuUL|-3M#aP=Rqm@%Dy&wWFH zq6x4~6+;A~O*j++>TKM#LhHCfM>fz+w)Nr0N1~7FCV=0*Pb`>hJ zjwCF%UpG@zhEWS-%(fB?GH-;`39Z3sb{^{cizWZw) z*Jk|MI{Vb+Ri?_o&Y3``f6@a~D}2ZIc-uOb7k(lluvkdfm`{0h=20u=m`NdHD7Aak z2oKd+MO`alcroW3NXnPfd!;l*Zw`d7Ra}0+f6F58# zK_g^pVTaom+oI)^T|-V@M3EINbzFcIkj}P2raOfAQtlYls}uVXR1}DnYU+d&Z9bB@ zPEAfag=(p=++MjqHL3x6=)1(^#uh(AqeX7LzLUiZO>BdrJ@PCXHK8sHHpgg#f3bIp zz$5Hk@_FcrS3@_2@8M9;>Bzj;w{lPA1KFcgiZk|mwxMIV`BKZNEt-)ItK#Ml6H-kd zyL(RG{nkXkZ{GwH1S2f_%2@dCEkfZ|GXY^TBxV4Rxr&LgFHG+0i0XPJNyD|w9J?l2 z(MNJLEDvBdg;ibM>dv8KIpsuJHw2oLVU(MAXHUeS>Mp(WyB=dNk<(F98u}OcWGy3X znwfRI4Ll)U7x5H1f?E#{)x4_lP30tVR^-LL!D9k}l-{!y4hJBvl#Ky%+tKn`B2-Ju zLsT?qXm*Wh5lCY~^h^&yNdxtU4ji@OIM~uSyO)&rBKx{FuU;^XjMN=*SO0$D^>;0? zsUl%)UkKkva<1pk5e0C&GL>if>gz_eq4N69qPr(i!r%tA?{`aTs) z35C>GaBsWS$HGPTV&Ew6MdX#qqMS(vf!wA21%R5yDNS8P0sLFdU}p)AGWuoG(u7s@ z^!b+zF4q^wTOV52(4)VORezf{{-z%>(lh+s>&*HObI1QxbJf50I{z_G{j-q7{tqea zPvxKe-|{*~DoZP_(;;v_BA?=PA^cQ)fr`4Xjmyd0bJx}|XugIMj>u94y<$B)}=pz#^U5sgmPr8qKe#nUao6Fc+tEFeSrlq4Dx<7q3=D!$@C^ z%2ij$8X_Q+E`Rhm`5zK+tIbxN;a zZ1X`<+(L+zFQIlJ$&3Fud_&Ju`1i>B#v~hyuQ?bB#ol(y6+w%6%X~ zN@I1rhMR+typ2CGwI=b26l7d(%>Hj_$->i;vi)>0;5QyTfzkVP)l>cA zQFOu(p}|Wc?fLQuA$}sHJiKm9!t6q?hc=v7F*)NVt+MfK;f!*%&Y2`Fq58`XKo6Ou zA=Q57C_i!rFqqX0W(6}?BJL=>AxdXmuN7&!ET5iTcIl0m0?ZEyBG%lm%eaih9afqq z5MK3h5-aR0;=st}72x&Is_H33H0+!8$P%53_R7$Wd z6Gu-MiZ1(4mSit^w}-1GQ;TDlylA$Ka-=pfCii9O{e>pI>r>{U%Bk=Jjj1z++`PRp zkKYjpumN`h{?yzUnEsyRXa676-2VE9|2Hb(*#E1h#{O4Ljs4%#)HEz? z)>)9Tn`}N&URQv=5r)y4ZNJ*G0!!nDuEVMY^EV}^`Rg zf#{Om-E25acN${gU4Y*4-aB+*c;R?G)nJr+PpEF)pPuZ6^#g?Yqj}7yBQELK?dqWw zg}D1cB74j)ev9fWp6?dH7p96bNMV)E?r`Ns?@k@l3qvdoUW5=;8n7!6-5Nt&2@W@( zdi*63IT#Y)evpsz6BeS}SSzZ!I(t;U50T~G8%L*{_EC>(3^a@&{F~y-dN?Ik3-~=G zX^2RcY)S^~c#j)`ie#|Z0;aafVf*`!CeK1RVY!I%ZA<6!f(1!jxg%(rXi&*u7<%jP z$V*aQS_k)m8Z;smSuK;@rKtnEOg}KvggYlu z+4~TJfEBposGd_rhOyWN_3L$tzDI?wQ!%EqG1fQwVS~vGFk9kr^T@?}M5t;Yqp2Xd z8y2&P07~tGty83zHiq8seu}^epTQV!NjJUKNXIi+go2~o{qXlmL=m=6M-h{GfP~~r zKpOPl#w4g1zN(9@gcOD&w* zmIy@v*=Hm5kIee9w}zQkDAe+7X;^#<96)C>`M#pRMSiO*Mq6H!8B1z>wHgM-oOxH! zfG^`sBr~EogI}(Xvg3bH(|*h`ud5(;T`x2CK_j1zI2UKd!Wq{`Wh$#?`Fq;-O2SxD zXJouQ3Tg_vXazpPFr9FYchWu@WuT2?*dDPFA}VDm)@}x2R7er~L1z`g1Ni2kCQr>5 zFrh%`DaItk%U$Ux8!KgoD@sCIlxSpiv8`C-lzE=WG5 z#cyA9(UBTPJ@kg(8mbWao1YIAiktv|z#-k}({|A7k0=f62+s<$r6Y~P_pT8IrE(qJ z<$P67)i$snE0AU$+HL8+on3Nonk4JBgC3e2f?`S` z<=;a3H5;IDjtPiaP;)a{(CixYMJ( znKbGex8A?vSs%*LZlNTW4oiO?djJkrc)*&I;=UG z$|dfSld;QC0<0sX>h92-F*lVTR4YSKJzg?)2eeliF^V!zi?V#^M*8$MZe#%EbyDUy zZt*lf(D$61j$^#FhfM#DiLe)%ZIvp=Yz`3c5))-`@FGDc$#X>eUVp#=3=u8*YCx|L zM?Pjogqa*sx?p}+rNRwUd+^-4gCOB8mdk0{NPWyfm@@c>t)&d{XfoRt(x8uN*SK=< zs(Z33G*_NvO{;h&=1hKn5HY@9Y$UqtUZBW5K!yAt;c}*_AYayZ>%ShetGBeZc+#H` zcIjr?LMwBu4kH(un@iuY0&DR7wywy|+`>TV{l=hYx@hV$6?d}X`CpJ ztkaRoH8i=i7EnbLF%=GL7#(y9>Xb`uu&7my*71;z?el0Xcz?G`%zQQ9h#PozQIue% z0ReLa232k|!A%u|Wb%G@_xK39`gQxrSr9wc;FFdX5x zXJ);F&8{lL?jYuVK;nQYkoJC`yOd#_%=KcPs(TcVLYIe8T#MI1s;@s!7P6fYQF^+6YaCUM`5nEbLlA!cgrYqq@ZXL~rk0>An1j-< zEsmK}A_7DrdVP+>h0LL<$Y^DBxkAWEX`$^}hsvCRRGbbn`LT-{UPQSFOgA8B*^LOR zC1J2;00?AJ+N=9R8o7q_Ej$IlEaKG3nKiX@x*q<#CZ|XAA`~b$QXKfjvEW(Wedb_w zj1f2kv<|L-b;n+gx{k}oIe{`q{yFKei!q>Az)Wm@Jxi(W%u#sMt0sUFj$jZnS!Of( z^gci+<0lTR&iHu+J5%z#zQe@XmF9OVv|^m%!K?uBm@$)2^3FJVJcE%k)Ah-&8yYYs zY}d_SAn#W=!9`oBCe=h&eYdt?jB93O^S{uj4bD%m< zjxg`a1Il8rH4bwh*k0oF#ZiMrG#wtsw@B2pycaUD2Y|M9E;p?fg=-8Mr{_dht? z{D0!M|8Zgd^A-9B|Nc|Zpr`wT0{^R(@t}s5&3+S7=ZfeT6mL_H#%^=T+mf{ZI>2R9 zdI7bq&9%QiWvIsborHb-gG~c(OFw*U1M&D$RiJ+DN>b5Cs)O+q40$euZ0xJSS7tQ{ zE-7xYS~T<5gzCJDOMEycAh5h25?7~3itc?vNHJQF`altVbiSc`^Q(Z+!Fan4X@D@7 zI8usB^RKCSOjA;+A%x_enQA3EwAOS`*y-~8ge1j`V~dt*VDF(N+G#?y z>o+Xr)l2vKvDLVg^-(Lo?GssuICgs3GmMa71@~3nV-1l60@hUZ=jJUT0<(UK-PR2C zAkq5Z`dv$uI}n`vS=o0l@mVP#rZ#t|-0@&1#^c|2!W(3MI8Q0oOJYTAV$O>vKrqS~ zov-nTzgMWXlOKD@sH(qsSOixn3D!C<=_91Z;O~5|=QmW91i}ro*UQ@EFLSLC8+BTuaRTUE&YX2=^V$>bv{IV$1@OT7LttrXW71F|~f&RV{d|f2VY-Hg3PUwQZuZ z@@q+Cp*X@XV{@cIYh!ad|1a4Vbhj~BT7X#Am0N*Z9$GFntF8%56=5RNeQ^zSvR0oq zQ@6PEkYS=2Kj~C%L*ktBXMF{e?w32=G zRUA4}pHnks_7JxEECtj|jZ&$-Ulc^KL=p4EJ8P{@ci&6ND89in*$&kC`om}zcCXsjjWgqCgV>hcR;t5m-azU*1cBhA4w&qL zDMtuXZ+`Vkz*!*Fxcj4|y}A4Q%iSRSqMj=ZwM;x(iZ`i-XIYwGK8ul?#d`%Vy}Os9 z5fB!0mcUlAvne*MOIf0}5P03d|B4ZqyyirdP}Ugp1uY^#-%Zk;!S00^Wb~nfthViK z-$eu`#*6qu57Z|)iE2xo3~W3!s-HM^>-21V1-do#ar4t$?3n6ttu$oZ!JY&0Znr*& zVDp?CB&fdEw^~|-7RObNUUg5emVPi7IHXZq{E_ZB@R5MwC9M4m*euL{#!MnHqNFfq zg2$RMemeoQ{iC(C?lLbW5P47`G+X-Nth%W0R>j-RGXW>ls z*~oSLxQiPDVNS>=O`}ph)IAk|Zce*%h{xXUd6-`yYF1Xa1%O^=%2ukW)2+_$B$YvQ z8|B?O?zzw_&+Whb(8auaI8~WO`_rFqWRw>zvo@>FJy+K{#YGos*n1r&eR5_8$I8Hh z#df@2En!p8)w3-Mk<2AAA@21*iZlx&AMDXVjs?{#`nHVm)-InsL}IMH{1t4|TZbZ< zDW|mXA%s!MYB68n4trVn@wFlo?Pe%?IuaqXGwV~%_4Q@8jG_MX$D|+6-zI#3QA0o?Eb!UBA*gJ8* z(CysuX<_xCrQ3tBh&vJZ=m=30alW_tR7#Kim{&JoRk4~v9v;zvL+=x*({eTk=2f|M_ z908Z)8z{BCoWaNLXMK0?pE!au)ydB*kr^m}+X!2D0KzeCu)l>)eDJ%K=*$B6M@{De z*Q>vXgGA~gi}5pUo$W+oQTpd7R-3G@-|u3UGpDlT;2DgzDbiffAA?X8ksO_9R#v$= zGQ0#cyEsMIV6_N{80;9r8$$W5(--hvjHwIo#f)a1TAS%OKx2(Il*WVoQ?cRqIZ?`_ zC2-%6Ba1Fx8PfTq><{|{Yc;=D4qdKhl;_6Ypwb@kR0c}BI^qV1V(rwLl(z!af_E~k z8#eMdq&uYXbYHQ8exOc)XMi|-V%PAq3u0@MdHs3^xhw~Ocqcx-hVQw%HBX+DF>!bC z0WeYYvBX@%m4gvAr?0|ACh!v!@(^u%^9Qc|dE$zl5(S$kN4R+b(uK$^N`9JXU@{%u zP|U}6G-V*)jA3yW(p(~srh4)=o5#Q|TF5rz1f?uYzCff%He**YWhfi*T4N8k>9}=< zvDX3T&|T`{h#$;{-WvZ`)f$-Y%TB37!@5XTBDTUk4Uolq8b)Zb1K{e8-8B+-M{ zgQA}TJ(B|Jg0FbXi}lZJ-$S>F)8V?>xXmsG& z3M*6lmb8*vo8wmMsp0EhS!%Yp3%7o?+4ONWxBA<3B(1Ow^5}%5zl)7c-q0Pc`bH0% z+g3J?d>qYCq8BOKekq#8$8-UwmHCIjXcfQh2!mjWf;|v{Zz9oSE5yUMG23SLiYo1( zLqsr7Z!gO}tykYXZFblt?q(hvXfLXpRoR4zKnjpiA>E z3~S^Ni~vz@4cQz*s`KQD!@f)v@g}`|tS?D+ark(aP13<@N0f~oaYTgdSRIT*HhiPs z;zUR5--&g}=v_`C>Y(OxA|xEiM-@kQ!n!ZI>wzZ>Jp~5}KJy_^}WrF79x9 z!9I&o)tCl?tAmXBe3bu8fgwpx1j8%!3AQc75dXiRmw#%`427rJfYa0cVXD*c^-P_7g203!qB@G`7{`91~H-w4tJ0 z?dJdw!3tf80f6;gqUiQMZpQ=x;SWu*Z&hN(f_vS6JYNPLLR24m&3B?bSnCu$2B#2cVx|f(ag|AWS%N)IjV?rIG+BIP9@RR`EpI5C!coQy5>^v zcK?~Jnv84xz(9xyJ8n!IjyCO}r}|l_y5PWY_3YW!!D8Vi^sW6aTuUB4r*!*q|FBrr zXStjyJY@YjdR+D>eJPcA+?5DqMZD$nVJ~f9r50t_Fd6M}TG3VKC;ZNx$~w)r3Y@qC`Lxq(uafpV%+<7ew!5x% ziG6=4q(_TN%dRxxL)jHF%zB}i(9Nb3&6AyR1Mn)B!itHdfc%uCN=dK_s&W~YG4fXW zR;fzND?$@KmY0gW*g!X=(6}14?Nl#F4UD%d>;W#8oLl;3$v#(^4H?uZWGf4J`$~OF zDjoU6t%x!hgfj-)x09WiXWJKrG9CVNIaN%5&Lp9LU+``2zE}Da=~-smKb-Lq91TDi;S^oQw5zD_=7bV6!G)w? zh#pAEdFBKoA99{FJHv^_LYsT{q;{b+&5Rn!%RN-z-ELkXQD0^T4f}Zag$42r<>(Bw zBk~a~!>~wW`1Zous6Z4V3f3+q{gdJ6P}m(ry&88HosGN_@K)TB6aj$2p~zg}IZ(PY zvBH+5o72cSm-FJnD2DQhrvD?Byam@q^gJqf>i7NUWW1J)|LjnUy-^w- zRAoT}H4WlQ0fvj%CQ=41x`NKR$Q8=^dJ1>7jcMY&b`@Cs;m>3Au)Eve zFY(gb+a8H!M9O{!rzwq;bI2l6Fz$_N*dI#q6`e9rEe~tyg~sO|At=?Fr;LiHk|p4s zxUk%(jzuU^Mu5t7I@#r#g2gQ6Tykal)F%Ri{OF*g5N$cVF^MPqmKS#N;85+`32^nJ z7>ekq#Zz+>h&RWb!_7BNza`i)YF$nTO=ao2t~4`4DmG&Q*UQ_%1o7wFVO3&wccf+# z`@~?@C{n}9ik#Zbp&Ax&YOnQ(-H?ghlCR6jh`Mc8n8vD@342JdcHjYr;0Khrh+c@4 zkRV3?Jzg5-tkis)xtJjHif(h`VF=EY=H|D9jU&yZbfJ3TT&b50K=X^7c39LaEw~-j z`ef}Dr9y71{i@L9s6;J7m_g!Rv-CmZSb2zmh!^L7KaRx@u3WYIrhRY9{1af&Uw&x9 zNFLY7@t_Yl9UCbXFF#XBTuv1dtAY)Ms}^gc8`TVa7bsR=E%i_g5JY82!YE)3I|UdK zn&>PG4GuJ$^=nG`f)>xPtosHF-$2`q96x+(;(`)pWLT2S@m6hdaV(ypzx3n1$RaAs zlYG9_aTIdqIjqfIRiuR-o0g$KIBduP+t9X?y6lb4W}sLiCW3*^bv<9g zfzh6VL@e7~HX2CM*wk}r`Ip)$nx~0uHV?NCXeTgNm~xYaD5M6Ka93IS#-yL^&--N9 zSAWd7tP5CHqKe{)AsZ85S6|zmvz}zD6XFy7MhvRNMGe8-I`VT^y7Zf2 zXZSh&w4;6*?m0K#-xZeB0u{Gbm|2?ImsOD`W%pr=d7_UQ>|HR|99gaGT)@^Uko5ev zl`DO}#$1ZbE*oertfDEt>xLq39$HT$-3(sj8i!v)TjTUK!#AzIp%Z2%q;;x7nR&ZI z7A0hsyZ@lVDk{jskSOj6>A{V}aLnoP+Ya*7(Q)LmczX`(@0^&IY^ z8XjJbo7~o{^X?%|<1P2K{qf)}{#|vO`m3p>L&MXv7h2}SUh%wxSg|wQ=(Tb;m;sq#Q7>0W9Z%I}nwMW=84%s=4O(fI2amDK8k z+OU>$Oegz^P>*2gDSJMeh?$jmB=*{{-FnR5$L$G=BP7mcui`{*R4Uc2n2Yc*V5k~k zNl5CD3+)x%Rf8f6vo+WT?|}z3CAHFOOX-N zj@}IqegR4eTa)}RDB+)LX$A(?ziZm`bbqec{|A}{|64!B|4xMEKS{!0(~l%Ou?~E2*H(`B>4C>^WF~5D|*uN5%*KGLe`u8#c1du?W0z0T3aVG zLtCh3r_g)MSe2D@VgqH65p4V?>ck%1)7hpHPhIHAK6pf!00H_cPH0g`si#1Vm#YClU(H0oJpD=I7eyGSJwj%e%Q z{wgUovgUjVX?UfZ(72<6D<1*hE<1cDodT-)C^|6Z+1u4(b?&ep=}%6xc{fC!OZks5 z_*JF4s>ggmTBD~~qr&Ec0q{i4(TXG-^Gtmf`0)ZDL2bynrhZluU&H&^~Vm?L@t+;?0vF+Eh2{MpOG?wv016`S>J2b8L zR@AB*;y7;J_wT?Mi7AT`T<0fa4cB*DD^FaHqWNWyk?^Q{z=2H zb{`(3(Y&c_`mxuH=hc}Nv)wQOwV#3A{W{5a>Uu;dfRM!&R&Xs-fXU$+-Rf_;TEWI2 z>fohOP)ruuRFOX4jDnMV%zkO}Ed6L*+Tw0c>jYjoqC&t~R$~ojQ!JrE_7l#{E-L`A z^zrhxcmLd!BHlWkW^{gKe*bzMW@U7?|8#yzj<3Iucai1u34E{lK0(mqm`y46(e;2( z?mxtM2>ZIe`u=KXK^2ilY1g;`!-tB4{HAip_mLrV$96|R@tPRwKL9o^qQAio@bN6m z!rdC4@bz%s>p!3hsxQIqisnis6-r=Z)LC*j$wC`WKZ~EIG|Hyu$oC`&Rp!UpcSr!G z^!O3KcVjqm9Lab6O-NML+z;{Uor|GqxA|~7WTYjCAOSyw!?M$9aT@5sGn~GC+q#DB z!Os`Dl-_$sK=8D{8{j9<)w}J$S+;1NUO6igwH7py{`6vuAoWZJv6T7}dP+21O*RjC zi2LN-fY)b;IsjlNk%IB6;W)9%K@=i@tiAyVMANQRod`s3i#M1qRzw|LsAWPZV7How zH%V;J460x?O9`uBUO(UYZYD;GmhR74<@kWwm(`gwZ0TgEJ7bB z_>5x~o3@WW^r^?7AO?!4EQf@0&E4b+RFiF{rsWH2<|BdvxGrD}{Ed-!0_E%Mde9rj z+g2-8R!B)B=OkMv?-FY(5DFHn3YGEAzk!+_+3xmJbc;ltOL^hj7!rpE(z{h)cu9`d z>nB%E#3K1}UP5z9Kfq7Z8g^-{LP%f&e_}rxO?FkEwP_xpHzd(U z_TnM)^Xk_GA-6*`nhzW13(DJ)))!X9aNme@m{3x+dY}n4za4`Q$y^sg^%X8m+Lkr* z6?Sqet*t)j42L{PD_3z=^VQ|WgGV0`3m;$v!<(`klL+uz>OUwVEOp$_fe2v=9oHJU z{5IaGlv`SB>bf%8{WOYvY7dP=x)Wa?yFz|LjWu`Fw+KsU>gx07vmEoVmcx`BMM!(ac*hn(>nUyz%mQu*6^o+ zrFG$*53>*_-i*Wk`ILJW!CX7nMt=M1BZQW|SSIiGPxwTXi zpxssc)t_aqDweQiSn@0ZPj+Rt3}axS!*Sk5$wI3!u2&V>hUxUVs6EqW^ICj7_u8;4 zPSx#G;TkIIh#U~S85NDp{||NV;H3GQEelt5*|u%lwr$%syKLJ=S9PJwwr$(C_4Vu% zd-k1}xiJ$rz7zM{KjDqv`>eGxSFTJ>`?6q#YS{SWk=)-d;!Q^F0NQ4TbkFk4UMSNP z`I$k@b%NFDOuzS$&udEuWd*yD;XK6t+pzBB_i)1L688tcC#eM4C&&3Pxb|OLJO=AT zAF)MEQ?@p-r~2m~ra_Jl$kI^Dgc0racr|@BWL_~APmT3F42z|qhC!_eawWmmB0a)< z#-R$aty`m+2>N8-wm}jFgDr;=eY;^bgWgEDX?8;9;Nsm+s_udnUk~4;AHNA=0VBZZa_QO z*RvsrOghFGN5Om$G*~jHpab#Qpii!9vZd=_84Jl>ttE^i-bkBf2Sucf%Bd0*0?&ei z(kxEJ4E5iE{sZ zjMHlvwcoGFD25w^XfCYFHKyp7r0HQ69llfXk%-vpFHP1`Z(DH}D>LnppyFB>;mGXH zy1=66;ZSdi34z+}eQO1*n)PAIHr9lxU|ix7I`fR5!xSzWOB|a7h>nha=9PCP+CS_2 z*U4;sYx*hxG3+`wC>ZW~q4~eXI&dwy9oRMq@3kggiaMN4>d>UV&fnfINQ33#Y~@z? z(Ti(rADZe4;q^VnyR*O`jI+ztp1AIqf#LX8WU>+|Hr56R(o6El1IC{b9`8l2Gxb60 zgNkwCT^8|-pfF&##(6k(OSxGPuEIQNCWiLM)B`%|dMTba#eC<2Q)e|=Y@QP`=mt68 zOEQ0elD(mraMmU0F#uDxrm$E2kk_3H z7!C&p3y8gr=|Td4fED*n_wmqO7ta;&&@OGoFOVE!P-oY@ZsmAc!Z!jyoNujzXpFY& zW=$8se5-$ks9x`fs@e@;@UStsJv`*{>n#oPI6- zdJXb*c;1P7IW^egFUUkOAVWL`{+#<&z=HA^mx;kx z6Xk^B-Z=!}mJX3MFLGbn2PfyuEZo>jUO~?okg(3l#Tm3ZPCYU}l`I7CYc1LVSE8=+?qK2y zTz$d}@e;T8ibyu3;w062vR{VA+(#oQ*8Wo?Inp{| z9$ro;m1b(d=}Mex1mAl%eu5ZiDP6(AnzUtW9G&LwGL_%hRoSO`1$|G`77rfz+bhYo zZJhua39)_UiMf&KtHYeKdVqh^D{+#trV&0T6Hk6*5%n?u;nIsuJfD_vaCx$U_~;}` zZ&XkDBCe-BBSO8+I!YL!>1$(jF|dvmm!yQ8Z%yeTu_3Gr=g>om3L$a07A?L@tv)CP z0i8^NOWRAB`WveOpWpY_?tl9Th<)dL^oUq|7V~dTiZbC02!UK(czToQFb+2DV`Gg zbz~lR$HN5Syj*cQT5rvDa4Jc$?k0sL1h< zt9o=%=Wqn!5>eL(g1cNsI>+yA!h-tFP(1F7P;58UZvXh}!R_g(bj)Bm-fnv?ZS{QO zD}9bQ6W^3Yt^t9c&fv8$al1*@u-;B>sb~8cu2@H!Zn?FC7NG5&#;4z9=(TUYSV-He zC=Zf|Z(RBw_x%FBa-gPE%?;q@wDp4h@!ZLoYo({VXjgk2AahsLYqW*-zWcr+4OZ_R zD`fN3Fx~m|UZ2o{-DtjIPw4hCiQMG1JZTkKCwBMIr0tmIPgE3N<3xtdh1Z`Pksnpw zi}6wV%;Oy<=x>6@cf)x2o7bw|BW4^4Wy1#cZKBaVLP_@#Ia08=G9nc;Sc#9*S?33$ zU{nReDNl9Ks(SVv_Y3dkHCZ_xkk28MQZIN0$glCKsNZv?gipe~!U!e4h06w~9+u+? zJKm}+_=)Og(mZfBLU0iQNfHN|ZriW9c0ej(53d|<%QUro1=JLnZ=R)%+4oa-_R7Iz zg&uAA$AUc8?l?G=5+c3{XiF=gy~N|RlH-LuT^lx-4j$_?Y5_~xl6}24uW>z{RLz2Y zLkICiM1>TU`0;BRM#yJFnX>a+H%cZ@^{PSTD9tTZg9p|XWj`_w7)PcOGHFlruMTQt zz(Q%2)i7c9`WC7;M<6(SlW^2AEB$N@o*FIFk>S#jJcM+(ha702mKEoZM%&*GL;Q9I za_D*Z%3zoB&|wu1=87Wr34+&@S;fM5P-ffN;s^U*^)fzJ$Go@bkH&_+Bp#2lGSbD4 z6Nn|iquX}WLy#2dD>Ij%av5uhm#sD*a+%+~^~&aenuow=VrY(ExPHzbK5vUG#rPai z!V%=Ca$AXVW74qCeD(7BUKF?rA4EvBXlgYvDr?CgwT+o0re~S~!(P)w9|6iKKV$=T zVV}u%kiZzT*Jy+nCdApL*~cYYag&!n$}BNMev?-hTutVcN1mJ%G(n;P8=8>t zC>R8m>Qlfa7>RKKmjInsgWYLDbs>@>ST6^cS`)^vAeEz>1<}OE6(d<1ThB7%1dt<}*|Q*;Hb+`6vW~v5oHqtVrLI zZ>E?MQ$~W1ayu4bq*Zo*wa1g0mDt@rmhNb%1;wEUH>cum(@*NDbCM#}*f-y^Zu5)v z39l{^>bzG`>bdF4TNsU4nTztz)j_ZkQ<(Ey?7gdIV9L@p;@X z^Uj+QuZ-FLAkl|f`BiF^2|WJu`_*a1#B!N%KZX*R-k7z(&9g#uS{(X!y5?YPwMiQetcY&#Ok(RTc|K|diz3id?~Int@7pFx=PCtTsP(-)Kg4K8O<-?Nb;5zyqmneO*& zb4^}@Li%zQAOe|0yf=&y9t2^NtzCUN_9pD6CJTK?re5&p4kF5S zk*|PO&tLA)*4_C60aHPM!jQJJx(yFq7||`s*1w)yVtbHK3`t8}NIoUhtLlJ@lvCtU zf{_P^LL-Ym{T4~A!jm;&>(Hs0Rpom#_*u*OF$X2*7veYN*LB@HaJse0hniY35NdZE z4)kk}I2pm;jo7y;gXoHCLu!W_VIEB`to9gaY>eC?AT zIs2i6X_OPJ*PzLKUQ08rTrgR7!u!}N6vvhh~1GD3EAt(FJs0<^>= z0gVMZGmTAxIr8$H0z#%@=-Ket>U@f~FMEbC7RhKT9GpytLN;#04^WyMXi6EQ zpT^7-KiPbmh+te+-K2Td#j=HyGdvmgsPDj(VKtJVrA=0tqfeJ9n-})U&C&r3bPL3L ze4L%rDy1~Q%SE)d{k`03^BcQ|uEhv&yV)ExKVaokqB zmM5gtVKt;P$I?bIG~>D_E>1Ax6B=!WP+QUJz>MLO4LAn8nP1)5x{&irj=*m?URq=$ ziSYdL7u8PU4A|$0{;auUHSB3> z<_oj6$O#xjXp!5#Mj4<2{Nr*>mJ0_=IjO2%*O(|~-1%?=3K&-N9hX7|oVt**cibp= zhgO10o%f~_#~)A}U6Ig4nw~Wj{o6op(>>l;1PJWgeNXqY95cs8=vEa93ISB|ZflFr zI_fo!pDJ%f(7^8#cgm*cGVKxa9YQa#eB*jeauzAWKif}I7+vV}wH(ip4jFU($Pau) zROy`B_F%}Hnta5snG0Ycat?I|n@b5F4Q~8k%4Ko?#bEk(ISVtx-!B0f82&HFS^kru z^PhAo41ca({&Lx3_#LP9pDZZU*Bmj$;3t^$YGW0X(J$8BaEUS{%OY^32q%S-z@Uk& zkkqd)5jFgBbH*m2sqwXgM-K|-GsaGwWS^Kv5hfk)ru-&T0__6rFdAzwjT*;oNdwQg z00D~}gX>220NQi}P0@RyYo#5-m02}(!G&qbzM#Q{v?)N&Nbe%ux}wGK$VkWO3ka-< zzA=<5?+|ung0B-9Hy!#qn;AhW>=Nec0Mq8RYdsh^`+e1gD}Jxhdm205ymTHhmKIk= zMj#uRu&chZ&mmcP zpm_~ZB$!Md4+$co9q)Bup);FPWqSlS6u&V$`{gs+AcMh}w=rVnRw^%sZ*g06@rixlRW7cO{74z6{Cj&(5w|;BrMhXhu5@ed zH#m|$*9gUc=H7hmt5BXW-@0CR=hLjqVxm>`gQ}u{k@rzuxA*Gt`MW}p5Dd5W>c%K> zL5-Nw1Wi^is7|Gew0?XI)7pFknpO;t?xj?@ZkPLeuxOWVx$Rxrw4 zifudvL>6N$6|Hpyil2Y_j^pZko0wt@Ood3QLEU0q<9%y?XEHL`!@zHQwF2jkfmQ)* zNxSk=6%C*0=%K6C%4+VJT8y1$EW9nAD-`hqxiO$GxT=hY_Z>u#4P>@fz^3uWJ6pBN zhw{ESF{yEF3H_X;ubdK<^wo4)>fqoT^<%e8QN0(|;U||OwJ7g}I-2lClF+l*L6u)) zMXZw5>xTCJ(By$oMt#u_SSuRGLyx0RO<`sllSfb&6MX>yZ~O}fOQnnDJrvDs7oJ1p z`5uO9bl_6>!ZnQGX6a&|3(@ca$BBJUP8?C#b4yG&>pv zvYttu>!0C&FV?z0n(P**a^!OED?Ik>!sV*H)bcn9(HcWxaBiR(HwbwjS@Sp$Q9M^+ z71ro%Zw&`iFp5*#rFfMp3~!jYD1k1|{u1R^%Ph_J#rZ(g8J_rv#$)F8HVDvol( zAdZTrI0m$`#B6)6h?N`m7G<2Y)rvZlHsDwg`vZ4Pd$KY=XIKkvKVD7?zs~W=l)wp?l#>eFSLG|ACa!=bAc=L8% z+GEb${PBHf1Nsu)2!oCv&XjaFmMRil^uoN}=cVJBVnN8vb#@$P2)jpw@hYw_>QQvK zn4d71*C7jSqo!feS2u?7>!F27erXO3>*HyM8$57sWbIIZorzZh(W5$3L#L{Za?cy5CP%K z=34{b2DH}I%7$Hpy5N?X1Q3@97+`crdoAp=trLQad+@^@KIfz-zw@>S7xND|hW2Ip z6Oot^2<$q+?EL!}CusxjT4W=KZ$n1{&RKh~Gfzf(W}ziI615K?m&lS7eqL(n3Xw{m@#?uk4MuJi0Qf|VjL8&; zhl(Uwitd0gWhohH&Nkn2a4(j(9D0Ew2GCv8$kf88#G9M(o&dzno-lI4dWIG;UQXai zh1Tfw7&!!{Ag0(X4`A5sU(vtG2w6R@Bq~9n^|c#hwO3=seCCn69J0d8i|Zs)2ScDL zb_5U3?OBmN);PGCW)|)?AhC*B)d}-8eb@wqo;9^cw>`Jc!e#u_Ak4x!z(iq6>90{k z!Z_O|_n0?AXlJhHAIti*9QpMFl-f37fQq9+UG!n1Zyh-?q>0_UXVajmmPs7c4@DG2 zm64Pc01sv<(MLVuCp9mNbSen0-+%n}Z(gItNh%EL6_U~d>+B#2n;OX`=V7cuB?pT( z{~ceIewnADu_AK~QUw35JpV{%+t5*d0jk{CTnlYYYs-LWy(Yri$-fOD$p};8-|lm= zZXtEXw#~WfeT+2aWw6KiJ80b2ql|$VrFpVCqBV_87rCe4_gd5hdB%XxD_IC}z|!hV zG1By?nx0KYeMxP=Qb$8M(@f#~hVwYtCH}6UYHW8F)^g3->OM&D?6_m4yzM-%B~QkL z%3L&_saMxD+7UqP$p>wao3}4nMnFv13Y}S& zv5L4Mge-82j0NsCY?4mh$t%YEBJvnhef%uKd4z`@ef~9_&c@A_?L3!|B>qSM=kot>hv!d zRmT5_Q5A_DlkKPf9q0N2Nf$cczRR6u3{fU1&dqGokDDC8kId}c|CA`EloXu2=VhZ8 z4$Roq5L3qan?duqGSjT6U2Fcb z6*~R-^xGF#^5}+8t|6j9jp~8E*92yCXh)+)_Fz4C)TWI`NiO4R{(SU@sbQpfhbxN3 z{)GD{t$48yOmyzm+)|4+-HhGdp@l}yRtX7PE4Y{gvL9QP0s6oAQ~ZT%{(?1(|BbNb z|Lh8s@t+V4<6mv{@5=lC%FX!qSD;#Ud#tFs_cL9+u{!$W*R((EvZY&Lre)o-q@kQ^ z;bMLguSV0vk`PJwec z`aS3E?alhhqfJveUFXBUAziw`xrP%(lk|C8jEuk)aVylN)}TOF4UPiHzd3zn(tC`E z@X^;y6#lMn%uwmjM1`|CV&NUtR6`K{fs3XRQE_=VUe{FbamCak*Y+jJ zEK?FLP|-jK@Vzf3WzWb*90M~ zxOfN+EPh!s{-VCe+L|Gdclhuqa|I4J6jgr}w}<(>u!r4+sQBTP4Un}Eax`bEam#M8I$NK=@ z#<|~%o+}9{ERQqBi#m)!&G$G-V7}3zy2!wFj9JwqgTLA%3Q-mB^uYyrD@=$X#5S|u zuL*SHVg;vxh*ReGq3gwE)*e~v^LRDqC|^Zius14TU{RAL9tWqmslk&>)n31C8G5>(N)WuQ) zL^!b7NK=9t?}&a7T;j9c%}fwofux14b-3J8oSiP4Um$_u{GnTgT2&OkN(}4P#DTh} z8FVa+ljIr>x?B4JzXB{QxUP?&tOj5NN&t93q0m=K9w3OrrTXkF+bd)zt3Vuc>fcEL zNhB{Z=5U~IUy_#+32Y&&AiE6Oz4MJ&nb#0g)ZD$7%_beZHzqI}}0Z!Cxu z0)91lOdq$3@er4Dr}Ds__kGkkjIumij(l$5#d!LT1G>nSm#1im+Rxp(6T-_XryVHU zFfO1uTUHmXKUX6%geSX?-?DU0Xq0tK5RIyIl+~Vh2lpftHt6=|hdX7_B6XjfjxxHjxfA;O9g>P6_dyXUtpn-_JY3cqmXM zyf${Skt>ckJFdP7OtqRbf8MceOxfShT5@44zxO}HrK&X6q220}^glt(dtTXVjn(w~ zWKBwhbUI{X!pW4QPxu|91yHC!8#v|dx9w+h`i%|+?I+>Ue>@~@bvXG@Q8FhSSAcR+ z7lb16tUMaXV>HOMXQ^??%$wuDU%m#8B8S7;zAjVXO>umnuokmP6bMhaNd;d_@;^5s z%;vvdjXgX#5}7G;6-b_fb>=1h=%i_sJ9<#6@wkcHb9NF0F26EA+GGVcCsHH@Zs2YJmF(E#ct)Dt&B6X=&T#v(8 z1pi1>aB?wE2L}*jGF4?G>({|>xP?i|G zp9onD2=OFgkjbvfp%79#DjoR($eBJV`xledpOh;j1Jhq>cgFu4YWM%&4j}&+y8dDI zX8H?1{Cx~#`p=Z_YJUWa(cBJKZySA6rn>ZDv#TX4`gU61IKQ8N97vlq;g5(BE_rrk z&evRrri&~Ej1or=El{{|kj_+R21^vwH~juGwZQs}=?zmGos3su0!qxEN;W zos9nmErAfUL5Mnei!f)FogS3%)_50i5J6Q#h|Ar>*~)%Te~oBT^Dr6Oca@~nibGOD z_(V-Aw3ms?2za#1w~Okz5wW%?gMNpW8(YTA4kiUk_^JXtQl_>I5+p`~ctsZ3-tp=^ zFl-hiECm4srJXRxI?R~w-te`dNW@f?bA7=g(pGs_1ZFtfkBD28oG zkGm%t>YEYwC-&|Q-L>D{n@w2Y$=h|QJ|oO!^)zfD7Q9jZwts3G)C+@9=V9e(v-RFd zx!vY%i+%9*G>mp6mJnE9ps_Nc*`X8sz9Rli5U*QmYC#H^XLKP}cb zsux<%>1Sw^?t#ta2bMWayqYK771-*bG|Nv0*!^OjXcwvcnA+NG+zCn5kfpWc>F@jsWb|LxrT z_XC{&M`(;c3g{X|$wbU}t3B2^V4L^RZ)>vd8 zI6(t6_d2NRnQ^P+8*q0+A~UV}8$BF?3;0(0nF)S(odnVSI5St6gxG^E5+C*u{3dmp zqkx4;(A#o7*=1T^b8f<@OmIQ8#sFk zt`VjXG70b2+y3lvT{HL9?S>!9*hAviizlAoN&u5B>f)t!4GHw`j7#{!_+7r|z z*JWD55RumVi`w#X{gU@-eeMVrm^}EA?j06wMCdk-5Us*DB=X)nuHLR00ssU>5^N9n zMMb-4IEW&zT>o2{a}E2Be-^ASN!quPBu05<3cX!y67iqHWHdp#vG>CSSFp2IduD0CL{F-9?9+7Xd3W?GzwRsT>a9@ zJ@_co84XUK^-=JIieyoz z`|x$QPs%&~2ixVHo^txj??X(>`4?^|jJ?A6oEd=WT&KuO1-$Xd0C!O=iu2wr}V zA%XhTNz57TqeRD3(Ro-eUqKeTr^I38WU=dx4`NABa2witB__j}qrmj7l}DG7SYR){ zIhOO&zQkHGyd1CSOo@9`7Hu*=K!B-3see(R{-ko*Ihg;xm|*&onEoH8`TpOpO_}~y zj{dPg{WX>Ry+ASl4KV%N0woeX1{=Wtdol45LF*up-rL@)67CQPcm#)!F4GPF#pfU@ zJU1Y7Sty23DEYMD8vug3AKN6sx5LXnvtk}cZU9Ym-TDibV>N@Bb5ERJI-R5g3XYrw z10lSF-q^*d8GCBtBqJetpzJ5n)mPtaas4r2YI=VfWBl!?Y7x(2vc$!+HIVx4C0N?q zk$tXRb<#YbmiZ3M#1h@cKYzYIe$gKj*4JV1$mrb({nJo%)i3erks?vXI>0E-vlVdo|? zJ)c)xSPgfXJq|pENX;tOM<9yrJ&ZFkO>x&T^eR~UzBB1W>qhH}EU&hTwMLZO&Fl;N z1D>ewnfvngUQ!VdL7RrC6aPlA8P%@9hH{5~(Z9t}Rxn40k=cQT9eR&uVpD!BcG}d~ z48I|1U#M_;Qz8;}X#AkEe7`bt$$A#389ac2QxYPv2ih)iOT7Vw;;%Xr6HS`i+uoOs zl#zX_dAUKs1HnRh#uNcDb3R$ADwn`l?qEJdX$e?&f7+HAl_nO~9GMM)5e zG?-2*lMNQZ1J?JZTYvL+0PmXos%4Gnk5k@>Wmru|mB_@`bDq^QE9MCcBy5lh1+LiE z&R|{()6#>EVTS8l4s_UN{5eW$r+Jnl`I`v;F=?~nMx=EWV`y{-XT+cwKmE;N?inJ6 zV&~i`zv#tSx*Pyfehv91*%#aI?g8kArY%1bCz<{pbP`GgJNdE}!ta8$JA`~9N@9o%!*A1Vv+mn3vT0xQ4_K6MzGUk!R!&6PBtWX@j z$HX6`VBa&bC!kH6?oOnw9wkoTjE8eO`dp5-?%nW&ln6F$?=_}L5c;69u3l!e_Kd}- zUvCB=vtA4#(wccq-Br;ANK=c1r2n7eJ+?|0>XZq z7CSN{zr(Hd>F-2cdFhwgRSg{4RT*1oYmxj|Cd4nT9)B1fH)d#YB|K7SrV}Iy9^nhpBPx3``z8Ofg@X z>1Le7J3=;+%hu7kdJ9D8q^eK6*beQKWw>f}le6!A%oNzBwp?q;KVRBzfN}c|+J7_w zmzJx>P`eVi5q=}LoHCqqQsWr3RqsFytyqc~?&0PP$%HnC66+r!4-5uEL$SPsCsds^ z2iJBhshcr=bednxw7&HUA`89d@~K7JnX%@3)pw@H!s!|a9=0YdY(a(+*2kuAnJ&wDbhrG=AF>v^~nT+Pq8Ef^ZS52T*iS*AeyvEGM<{eOEbp}6~ zXWGwAT_o3osEAp@w0^6S{b;W36&KuWhulIltu%OBbl7v?sKOe{~8u62}k^O?<-w z{%#OKXg0~=6yiMW4xhpIPVHJDS0M-3WYi~C3#DtLnu=$LuCI}HqwiF=d-;CL?Gx>;|6%|LgN$0{Ba)Cwj*jm- z?y^2+H*ADiU2JqbWI!K?bbMaCMi@q`-S%+^uJw2VDE(Iu9GtTb^V+XYmq@u^>ZMsD z#DN3BhLo&zY;Kr^!=x{!)VrmP89&KMAWrro3s%;mm{3Xkj%T8ZsH1*Kx5lF`j-IVa z`n^wzR;!obmx}1 zBAT2>_tLeym``$u-H(RK_A>0Ek<`(`7arU_Mm|g|ySf_Pos6m?N=GjXV2Oo)C9HOk zlG|*EhAuJQo6etOWk!gHxw@+N(IA$N3hdZRAValgQ&l$V&g*nwuhz`Aqv6bB>blg~ zG>{%MF+x;My*kee8_|8c1uTpedvq}gWnVGMd`jWyDYvY!T^>mMrqLMlWBM3XObRR$ zUpm>Rsr#ZN7=019Vtou9B?DmzZd zxIfgCg-{NY$~%JyUyH2~it*h?5sJkemJpuFvu(~B7EFN12Vpb@6H+ov z^D`eaT@zW(1(=0I!1Q)f8+d4{Y@|2{Mptkx3Nmg648L5Hr4dww586u3X4DqYJ)NQz zJhypMC-Sx2H^KqRWZ(PSaT;UH7Jr6mya15VHxA1}2e_S!pBReymNrA!NyVyYNxtp* zXxL#%PgETFDj0dpu?7QBlF+KuYc8dH7f1Yn{YcJ%_D1sZ=Ba7IwH&pvZTni^ z^d>ooboJpC;4leu#T73sr|cNG1wevC(!`}Jl~P{q0Xp&U$X!T4apFXjk?BR*EK;=1 za5Zq8=%jNq3AGVj`3&T@UGrA>dSXW~K9X}K3MX*qUqdbu=Q_rB{){S zVo@e~j@ti`*=SS2yf`!TA;ti~?!os~h2JYmXQGH0jO=52KI zKB%lljwfJutjz$aOkCIW_p~TQDX528(|U`mKlSu&$60{!sGHL zlcT@2B^&tL@Guo*ggFn8`QPoS43+i7LShPdyUPa$7?dzxR)aVLze;V zDbudoh}h|G<%-khee>GIdOAO8t9_wgAM@z^;GPfQuD?!?mH_p1Iv_eVIyy8vz5>P? zTg>~?ppl!!urs0V+CuV2MosV@m!{8*u9~~Mi!T!LzppYIs&a7^_l=-fgtI!`xWkKT z4)B$D?NQQ4b+GpB_Eb&FU+Z%?8KL*fj+^Y`4sk|2+yg$mGwJ(YTberYw%|8a2we2f zP~?xk@Ly1b{x8!}wm+NXe*+fz?{S#=qv!o&7yJ_=|F2!}Zy4*}LdgbAt=P5S4pX<} z`?dUdr}f^>0Fh%YH-`9dF74)M*Df?_uC0pKQI%#u_O-tt00*4;6TNd|e(7Pj0)>oKPUlvc1eMcnRX|9jzkq zezR%)#6=eLq|%ik6WjiAVa33q5KLn0o4R908tA2`5Ok#rmE>raOEE7-DIrP5HB^;m zwO>3ccx*PhlbFBnZNnKmj@o}T#A|@T9&%`*!Qk~R?Rjd&sI_a??b5dK12l#(^B2Bq)q}ldoFm^QM%s=bEXAJ9P*;Y zD%KkOfTNucQ9xf2a-X~YMRU7gNwygcAuALBAj_`WlP;BeK<1hGC}3Ckr5kNFkdZ2* zko+Rq9$|q_reWAVp|)U1HBmwyMFYK{-6oB_4Qd8KB_pg#tYa_q5)zET^v2&lbW4+3 z6G1FB-j*Rp@E(bhnlq<<5b@h+9?AmX!SSljy;tx&>tNKUlFt%FlHoiFJ&BN5L1VO7 zJfoy3Q_y@=6>L1xx|~PMwOBO0CvchcSkqU6`wcf`K-n;m39iwiidDKj`DqmMTtn&3 zanLFlG5TFuqxz+CSgR@lU=;m{n5q`*rbUuwzw*PBl3Qg9N!(h??1?M8f82L7RGH{D zjSW+GnSd}oHHcHKhF;pQw(JfjZ^qm%E>gXPTyit$RV1Wcc*grsyOu=YV$fv4lDXsB zn$@MD97Gh6;M}Gn9e7>h0(VZbnW%>!cUsR{!c?3m{pN6~akcEVSQ*>GU#UC{Tg+N3 zo0WEy^w!ted7s4jv@K++i7L3VlA^2K%dxhcd4zuIzQnxA1B#@|KNwM-ggvD0yQRT^ za4?Ek!C*9vhmy3*r`V$f!ye-n!!3T>(U{-?CV}o}OP)!nKp_74f$}-e+P;excp?iK z6NRxvs1Lupd?~1shT7XbL7@9*0DwbpWn=YdnDK(>ard7cWTb*vA zkSJligpYiHs6{Gb(f#qzM;uFQ*70=93m^zfa5!=tQ6u8@%0j$uT6Gd8U?l^$T8-jq z9ZrpvoMnsUatc5NH{}EIK*ovXV&KpotpHp>i+R>~@z7oK4=8g}5g2n7q`gXago5C3g7G8S!`%W_#%`KCIiv=r6tR zFy`QRDy$k=zg5>lI*MU4r+(v6D&^{fE$HPErd?SIfB0ywZT~wrvK!!99pJ+fpOQV9 zCR~%c@Oi&`!J|PFW5LO1D$r7XM+5PyA+AavmPQYSG6FJA*7xa19n#V%t_-q(}8igX=68tY$-97;caVWPs} z!u7#MJv(Vs1bO(Z*8h;wJyRXQ@hMPSEGBCe5YVL8>Eq_Yu{pCC?WSuGTjw|Ft5-pwxsl&C!Z6`;bk`klLc#FDYwkO=1y;MZ&K^0Q@e$f{arWem_xNGVngCXkqRmh=K~n8 zKlFmCbjj-tzGeBzc`+sMG=nYk2XluhwGRW%q5+kan1rEBm4jX?*!#xN z37p8!2;6mPu)sr7I~-wKO)oC|69tZ2+B4jUMfF(NR=TY?lBs;goQR{P#O;e?Byi5b z2$ODRffr$IZzV`0GMNFKkGjm6g^97epH1(lM*un>K%ZWZheH6-wmD#a=sH_bo4x}y zop~e6hH|CFY%b06<9sgGmn9GmT3fx5U7CfFbn%%VXd0&1ZOhF+ae zyKPK1)irw5XrQUtgt?Hc2t8vjI2xY)KJ&esdeV4MI>qu8@Vleoyps(Jw=I^;dU=T( zoMBfXhm*`X$H~_x%04=8_NkR3w|Dt6*@zddmPnIN{Fx;t_;mfoVmr~c10oJ68tE$( z6np*&2{1NvSG~X17#-Di>U}?Wv&%u4|I-m@u^IgJ9;d+nN8LLH>DFdjqG{W8kop{D@}E1= z(ma5C3SM4Cayerag1HW2n9D(9gRlsD?)Fm1eA%Zq-}!k3Dta8A%qs%lMz>^hzg#l~ zzKck1c%$S6-g(Gii*04!n6^}6KUL=d%OGXqUZ@FC@jt)HJTc}eAopKfBxa1~8A_H* zHeU&2A-IA2;Dn->tFENwg&%m(V2P|~8`E8aHLBKYk!OcAU2Z{sS)wp`G3A&uSz^dp zCv}bEpvdP8=_BvA6@LSFr`3J`PhjtVG~1b28UM2j#QvY-(f`}JK!2^@{;&9#>3@>2 z%Jgq0tZHuBua6?qNAW>EYZSo$hUX;<5KZ1VRNtnHtY>tPrLSD0R(K9fTV|vGP>`T% zy{^B$xVpGfC~<)zB4kLa8QaXir1zudiAyUoH16Sf13cw)(Q-?D{|$ z3`&31WYFK50{T?TnFQ82!pkRD+%vs5eI$B%nK+zjO7K=pGriex`rJF)JooSJ@Oy6a zAKyM{3gV1MrkCA}rb+1Q`9OGiC;^Sm99eh`61}rrfq|P9|1m2~?*bLp8`Z35^Q%hVV5glY2ZXNv zn$h1V={dJzip_4047E+#wKxr9Y_-Z#c9FBPy?dC%bvaz|0fc|tIi^Qz8%4g|+C~?l zR1Pa(=<*GBZ6EGhq}u3@Teiy`R$@AJB;4pR;MTFwXEBm+0!cP-)+Hhs+HO1M3AlUt zjPvN#M!_+%k0d$6~RHgoif^q z5=|S6>ijuXV&quE8oL#DUDZHA172zAL-!z8(5Fs87Zttq zB@2V&fz|uFI)p>kRX`;(F;wI}e@7Af7&O3A2^hr%(TItG9uhnhZ_33U)DPy=JqFCR zY&a`9?eY~s2TezB&8J*>q4I&!$9hfQ zSF8Fm#<6VHSS)RT9Wfh??L-a-MAQSEsBE^b=)YSGUl-Y?u+F0$?nHi5Q%$K4(&Raq z{cKC{4e#9*(uD=%788~2C`tYs+&_jnsphL?q??bSA_6CUS;kMT>w z8vT)^acF@I(Ubx^Cq}hX<%pnR3L2jRc{x~T_ghT0y;XtzlcH0q0?LJ$h^m@q4wee5 z;lY&~xROVo@FI+k%%Q6P~CcFd~e8b2aXJ* zqzfZNgs_@WfRy@qEKT}S`b4|3S)NnIQ#K0JB!m7OVJNA&bs)3Mj!;&^9Zd{!z6@lZ(l2!xuucnlp2+trHRQ;gPp;WCV&)T%^0f>($@mMMyN zltM#AuB{$M;}rqSCwT1zUELxekJ{0xT3GqfpT|S3!KGVjZKL^vlo4{Bc5u+cOx0bU zl?yS55|vtTqcLW~4H{&oTm||xkj&K0xw~&E&F0jSs$(<*D(v>Iz9dtwvPKLB<7~WZ zmu}BVa4Rq^2+KH0N7IwMWX_qtid5a>)F)owSIKwc6w(U#^#$O@y;WHU?nc9T;_(b2>9`Dkw96rW51J&@*?D6;= zb1g?^*Lcvpia6K-6+ll6JYa!W($HCR$RkbqnDgrnCd*VpMxSeWAjfCRVlFZ1=JRC`KtUFK9!%{=kw;vUEj-DCNiiHyJQZF z4uS9(dOULet@G(1(k*Aau2E9(Qrg(G+s2zUY+~^WnWb8AQ^_kGkJN2SiXW@d80*x23pa0B97m)-&WNj z&ja)rJ=-&1@HJ=5Xa535|BlN3)`MkYVfe>lQKtWUBOLz@J^u-e{uh7je+WmI{_ary zZ%2|^w(%Phh+>FB7NIT>fmhJ@N7(L4!;W2_SON)c#MFXUjjQ_!5cu89Te!)X-bUVy z*~HyAH-o3?XLXJ1`>PSSqKZWEo4UG>iwp9qZPyK%^xr@GqNm2zliQff@-qF=vRC)- zlflg!Ja$a|hRu%C$z}Xe$(hOU!u2?$i-so5jd6I9L^abQpQqFFdA)BB zCd$j{{hp=1ijIw%805NHe!+VZ`1?8x`*1DfjBG$JtmcT{*)nEi{ER-R=DA54mAUP1 zKOJ{1%o}-ARHQvK)zm#T^nRMzSMkf9JQ6$*ZNL-2MEAI9!KK^5Yg$OD^@>gu@9;_T zVw@TBI2+@BB~hyXY2c}Rq^qeu-aTZjnRbT%K#Z1@>OyTRDRktal%LBR9KSo^q6q;vd?j6=1IvEC31~z_Xz335y3)yfB-}w~1nu(8s`>PhQRIfdr zzi#aWyPOz0)&mkWVPFLl%|j0auXPbV!>K)ZbZ>576}N|&Q{mP$A+#_OodmBJX279#xXfcyQ{KZs%o!W z(<s_Ykk#j=@57Y$sg$Y^@Y9)OHXMp5h4od6mO5yrH>CsDM6n&{vOmloMAq$({rnx z^hB){;c%Ul=8RI%dh17PKtrh`u#mx1;v^FT`ZhB+P&DK#4J)F#5**`W$ zzPM1V?}1CE-uVJ-)#1iE!zPXt)=fNO-BD#4orrF-h<4RdYbIFSNwtUDtlNcbfC3qX zIPy+SAki8uS34=!-HXK&3Lt2q(b2o+jc>-mC&SBXqB4mC=)}MpbGB?+U0U?7DoY7F z6kRmzk>CK9^MjC+*JC;q{UCT5>MnRWkP3&n#cY3h(DhN8jJFl%o!KqpVu}t$#y{aS zUuxShyb8>_xctEdOCh(JRLc7C}`E;P59O0oYnZe+39gU;9Y6j=DA> zpw@0OCDHnqg6agv^U_vtnSe}`@H&cswur51lrKtUI8Usg?qM0qWC+EH>gt+8oYNPP zes_n}S+3elA{KW?=suA(x~7#;LMwF)wPK8RpP>MK)I=vGj|1e3f!3DdZf(sslH^2LVawc{6xWPa)d5JB67hx)`ST;F=&p{mz75NtFFpOiE((V?sdJZ`@uQ_N25ZpjR_ddiI3L-X z=Gy=;*D%r3!#o{xOLLDK1BV(!41|4KIqGOfQcrM#!ie0c(Vx3LW+l?UuTyq7DLx|t zPL_h6WmN@N&A;*;KDrW3d&K7MhI5WK0 zAQds3w#pz0(!9>o%|#2_*DuZt8QO`EClsSj=4kXf=Nu z=RjPGS+S zjV=|BFB>`w**tG2PZCTfeNoE>PEQ}NyPXI4rn*&B_}~@M&SjWzp@;Lk|kLSZ_=P0}ae+FKbTx$RG|R z6Uc?|6{p8p_DTUwfylhwANTlxL_7kmpA%oMAckks@9`aR1WGC*Xm`yP=&~KGsq26) zH!={f4;d#rl>kl@w8r7W(@hk;z(2rFMJDG8>K6w4yNILTY<@@U$(Vb62iUELp2p6;=ItKoax_cdq970#_}$FHG%mS zilSJqL6m;a(ZQ6eanwO`F0g0X4SKm7Pd9_d^2S#^*SqZcLo5CaCgDxXfdo>mbuN7B z*lO6*Za=ew$#P3=8Ra;W?5T5?h1c|M^FYIc)p~Q%O(@eVn_TyQIjKp=V*-$dCri{t zO!$S~`2&R&?aZTk#j&oHp}rK~zuP>1B{adDH}leBt>InJbOB}74??0)#LA`MclViJ zrP{kiS(;@X`}Co|2Zc^0F4gC11Fk>1?)S&h`m=wR=gaQGX8qS!aJGIt=Y*a%%pxct zQu0`yLwDaN{ZQ@ccvFAobYK@uL7{%MQuS;emM(QPGi06!LZ#NZy<4BEb*_f)&V+UD z8Ft;1d%FlT*B`$k+U^T`Jim|Aioak97O_i|*}>7sC?*Ck9+_xK&tdRppfI?rj}x9| zeBakbFc*sXo6_EM=OR0a&28^DRz@iIrHNL1x$*DDjWY1d=)^M&Z}i9Dg=*Y z$Z(#%B*s^#U#D%2F79DGmU4)a%D}u(u{>xWPJeWT?M92QWkE3tmSy39FhaK+1K1F+ zVSs6i*#RnHNbWpwp;_W&^Qziv=&TTr65oiz;Zz&(j6X*@d4x-2yQ{2}x^sJvwLu0a?23jDSqeZbe0Q1cJn`h^$rLj}d-+dYt_liZQh ztHOQT-{(7j&D|%h0;c_IH~jZfpWnmgpN1>>_t&jE|LW%hKmYs9sXzRFQ0c91#L#VT z7@$~EfW*hygZzx2KgWQ($)d-fD0~hwF*85 zyDnBdOtn7;=FoCmImpT(6;ML(S~Sr?o#}LDR$o>(S0eMVcuCx&$2k3?g1L!f@UY`^ zy5fAT>0!x&y_>h%Ii#3R^$FvE#$NYyPZR=FFfQd_e$aYwHXdI&<2p@)tyQ?2##t1* zRKyZ}I4NP18mlfWqNeh?N}RXPju&r*F6Hqk&D#*fTll)~e7H?NhJ-&R^&s!-SQV?w zyV2^DLrO%&Z@FQ1*fILX8>vj=FIv;fe}S+4lp+0B8u=HW`Wua8W@i0|D}?DE8CL&) zN_qZY+UNg!l`GR<1oEFKCCk66T$xz@tqiNb^gSXNqNqX?LO_HF^`6D4XPPs=nAvz9 z8wE%xdT11e?h?#$W2KSok;~UFxfR9_+3%-*G4HqJy7sH7mS)9_9Czh0>thk|z#Zb> zb85%t{rNR$QS+Ns-(D8a^$Wv6Q$Ql~@MXpS@QaFO3^NT|(GUS5-7j8pja!K=E+-yP zNFjl|;O6S^^SGXFb0>f9^&#n-yB;te28}X)CMdmIA1uBF77#Q_BB*l%gZX7F!zd2o zri?{|CSB1+UK~8ezHihd4ey7sA6&n4efjP_?7`B(?Pu-6fywIuY+OR9LI9VB0QChR z@a$A_Ll!YNpX5`Q#t5E4QWgL_8|Nv?!p z412dnlWGkhDnfdb->cpl5`omipnEl+zfN07xGStJBpf=M>BPP{&99D=&|o;(t+?;c^vU*Is0rE(RRb&Q?r1%jxpNyU#@#331?$EljV zgQ*cE^Ln{V-mm#Yqzg8q8pjMm7(ellD$N*W&^LCXyGCj`5Ft?xyP*8zf>MX1b4>QA z>_}*xKT5Q04LlCfS(cP~5leVwxq9`8ze{$)wyq{xv`Q+}pdNUzXl=A79RD)UW?r+Q zCz<|!lZ;n!3)=KI!X<%Pmu=yD*J|-h=~f5*LhMxzIK1cyHz@(42y!cENI^un##L`i zFGb*qIhEf*p}7bFJH#WIm5x%Ua#^hF*i;sGITtg*>bT6B+4(~009|s>+msG1()Flk zug;Mb>jRHt-5{XtGM{hO>GPZ`SVIFqtny#{C zz`)PSUzIv23Zq2mG&oh8*yD#xzcl=FqLB$r z*!MwvI2BBj6Qt@nPVi2npiYr?L1z-{Vn0xur9qm>`F=1mrv2L0To~xG3~W@`W3X&r77fwUD2u|83r7?38S1>u^DUe@KLXbifG31RDzyi7;EAe&3| zyW>^_rN!NlsH^(+lmAV-SfO7ZG7Wu&u)Xw#ziHnkf z`Gr;ba_q`H6#fJdCFPe724$?B7cppi;cL^V7EEhpxO5=2YMChBy(1`t(01ewlo3^0 z#1~8(o&m$ z{$?ZXtE_IeW2s661p^p_13izdJF#$?rA8!4M`~@fXiGzlrR2E0-zt_Cm89b^OpU=} z={kt4(1@l#5(a*8^-|G^tjtmh?PMF;-^0*_D1Ntj3h-r6Fu8L?`>d>gB&4OZd0IWXOI|NNujvClN$YpXy=6D2=vMlzY5p8+(bq&Gsi=+c${>nxjU2&ul zh)8CCqHaOVE7AJt7fV3xY2Nqr=RtkiqWg;y4T^2L`3o0Coy{B*8L%!C>evZ^*AvIp zl9I{nJcHN6%2NuxqfrUR=js8?t>Jg-BblAbT+Auqv%j+h=AF5S%b#kQ}NUD znr^ZaA_hb@OE#Qn%tDVk=R~_^(hHYw%Eoc*X$yG>b{v+V_2_ifUXq(}7t6V zz|mpR5!~})taoVT+QcB9@4*w5y}<#+kRmNaI=fiPdciu~_Lw}~im_ajlu{q4l#-6Z zf=c}-KanAzNM|Izm8qx-AmK|%A8Q5#@x7#2D;UkB4r?sulBX~S1`Fx2Q6NHxam`ik z%$9_QuW}VR1$50+cnR0oi=6_vBrz*2cPv-s9(D?&wNtgQR%%h!@Xs%n{SYo)t3jLP z$5_%qNpB5I3%{%ZU_pzjo-M8_6aw-6s)h;6Xk}RwRqIgKFHAo{-k~FvaouD@eCL$# zk#|eP61OTA_Wi?n$!3jWbgO_!8V`I1 z8{gDf_G`7$hPFJaDOu@qX)}gwD)20S5oz|qNi<+6H+D98?=hih@H=SzXQbsH? z7o|{&p@ex#IXr@Am->}@E({2m4@Ah{x+sHXwOl411j@L zhr(4xI-955n8-Bcvb_;U#$xwzKQ@@?<5yd#1PPR(8(v01M zH8d7*lo60lBq6XZt*bh!}_mUW+v8uPs^-*Zoe^%NWbwD0t6Tg2>WZN`LLV-&5u#T($LGT%)0;T~4>$cAiak`;vcUbyM@|X0#YZLHXz$Yw*+@QoUg6DAFq z-JyuV3EABEt8!hO)tA&~k&?W#-BqR!*_3ER3I<0Q2PBYoYop;-RULNBDki2)F~ zM`3o9+H2n5Bfv+lS~fFNMB$fKuX37{Y(fJ&UCBNZ8^9MLcnd~Rx<}p{yLLo8@ng9) z;W-c%3Hnw%r|8$@8aw04FK6|s zi9k~tog=bTvqHV$u=EH4u1$!&(|L?eK-r_WuWZV6@FlTg9|Wam0>3_1eWKDboX?2Z z9HW6;#c1wkR_p`mO~Kx3DrNVs3y0?REDSkePG|H^Cfj-*E3DLsEE%3O?6)}A%Prgq z!46}DlA~US>X+YWH{91@TV=D+2-T9j3v1C)OrtOZHZDt+tmJzt!Gh^oWvg4YZoR6! z55OU+Y95xZ`6#mtv=*YTxHfY2GKi~cDV6NA;-k5)xW;f)xAY-4w#h1^aOfFcY+c}T zO?3SM^>M~kMIr)p)S;M*j||S32FpkcRvi(Uh&O#sC0!n$(wXUkE4;t_ZU9vQ#BT+y zrX|?a9R3hAF4|-f9Ha`BC1?TbM}94Fp+2s>7`u_dS2d{T`TmG~CT3@}UPE>6q$8)| zV5QSW5dga1L6{z1Byu#4gfTQ+QM?zTV^U-=0rO{OU|lH0ViE)8+R6SqiCHaS*EW)1 ztEO}qd5?EFg;sYRbJZHiF*W`?lIc%N;dxX|ufR|fn7;b5=FFpnsk8<57+1ofcLtbL zd(!2#0XSq`k}jPO(x17MVQ;7^~XRc!UbQGbScN5IjJ0Scd-h&3-~9P zNC4GOWNtL6F1vE7>VPH{Rb(sC=63QZ-24zUUhqk($~7R!Gu;e;hdyva?|;;##79rm zSy`kwC!_S**hBlY6-k7CJ?0*(mAmE%~ydL&OD~v$9H> z?vj!A-IcRWgvBFHk{BryK%$7MTBP7;r0~E8rPWH3FF7&-1}Z-_DY``98Pw_q>xv5f zc~TrrbIRGqDSGk}Jj ze63WStz=1#9gFQV+Yw0-awc+ZR&7Rm+XGP1>kA3n{S!ay4Dw@p#`bX^M z#sMcbTegH%?FQAVU3R&$?o_x5qwUZsvLY=L43)gj(mXMh{xCawi41!ww`{TRP${=+ zm@B0%&y#c+vc_(jWfd}m`rJ56XJ;Pov>EHLRW>7FluCtVTC>M31U-^N?C@d9Mm0hp zi7XwiFY_-RQ{LKo&Mb;*7;q&6#?E}z!)o6}kSahUEelUxe8c(VSPm>s^R>$WtgvBT zAW5r!S_BYDUj}mLzH}4`b+wOmf`!emCfk_)Cd;=w;F5W~&?6y3P=PK^uAtAwlEM%Y z8wmEzdoV*O15r!)=Pp{;D!tCJDl6`h9&R%vYZ1a?St4aHBE1_ZUXg3gf= z!$eWNMNP(;bDa0-@%V%@XIFC7hl2nb@T_V$lha@1m_R9N;&yr0D6bukT8k%;Y}ih? z)`zAR-p%%w!YEc*K|gxm%T2pJT|cHKix6C!rrA47<#J6n3p}X;HZX#wg7;O9)72KQ zVUua=v+U+WTVQfz4-}}E?l6F*XB0_i)$;J;F$q72l)n3sSAlUqefemYieaccC{1~iF^hBnqc*V z8~vkwVX{sOE>FrdL$_2Ele8g-Wwl<4?AsxQMwF#2rer*gn1`A&atUCs*;K*4l}Pho z0eVAYXQ|r=l$ng5g@G)w)rPb#BE^)8;`wX50zKYw*)_hI+ASMJ( z-y(T^rS_iegMioPd3#Lv=k9cKxVlU_ND?`G zkn}&~s%^d%euGL^8?Jk98&~%P>TNiz5)Ez~QIEGf{s7iOtL=wNpRq9#AcW*^0%R5; zr7UC+#D$q2J}pk|U*&ivdhDD;cl3c$?)5{V)&5FxVa93OgaWZn6whe5cg$uBBR?>x zsPgtpn*?F12I5W2{z80J!iF5&u>t!zFSPSP;4-KN@cy3un=n8ZketW=VRKMhUoYJ1 zseKOPY$|hPrmwwL$nILwLKqFDD(mX&(EcxAT}`tijx&7HTQ=hfGDlJP|Nx- z6GLWvW?XJAF0G`HV;24w!gE!2>7pj=kMob8dlLH%3zG1?@GZFsDZ+uiYW8sl*&@;$ zl5j__Bz$>A_F#Qy`;^lJUZCa^5a0svCfaIgJb7AXxQro5^`9P9{LDX35rH0!y$we+ zgka9Ns@YX7paiPu*LJ@#YV)8JdmJ=-NNq!YhdfJvZ`9=J`6l=s;pF^b?6>lO2{6Ar z!9p_>JV2}skXGKU4CcazEhBJNccMnRYt?2qhw%-{$9WtVr(ii_*;O^Gj%bk@k99YYp#s1B%f=t@rgE85D?6tSz z#B?QX(wyVM4I2%6A0wC`V@7V^W*fE^mU7(If}6nPc5;e(sREbMka&8KD6V`|uOj1~ z#g4{K0T0cvXv~aLf7T~S089J|0UN%AM~SA)yxUyMH{XbL0x`iaI;$qd(VIS4l_^ja zD6U+7)(U1>b@;2*a#7Ri%56={Gf*zKIiJ(DWb;YrUBOVf%}|SG+^^s^ycZIj_gROv zo*N`#f`O=!Aac<=K9oHHp;?X`9#~e91dz^`5~rWfAMr`b9!ckNs4pdno-_s4#6oY(K|_WFB|wSZhX^!= zbGC6ZN9ZujqEVI#Y($4dHhX}3uRUIvx#JFg_X{r_N2T{Z5dNSeC1puhs<5=2m)5e# z-g?)6;BMnIcQaeIUQtcqz?X+CZW_OO22_M8_6H`F44LB^hk?5jaGIh%f*ZVuh$%C;5=SDtI#baJaDS7VL13BZul;-Luh);~nXYSb2>#;!~@Y-?nAN2DI63nL1 z9?^%=^*Yz&yr7RO@R4&(eM)?$Et?DdB&@3N-ltA!R~l9(ck(Px!5eA?n)21sce0(5 z<=GDe=_RX;FT^Ie04(%*m{Ym=mxRvYT7(WZ;|!9 z=B0f}bmzc|R|zE6WwA2kTv!mR*2uUa#N(>x<+UZ{@dX{Ijc$D<$;f3`^wrk2eruMb zTGCzJzN-1g#WYNnGhT}3KsWiGNfYM34d0P)O@$6wj%sw|vm>9nr}(19v5sW1pONXqG9ECJ!gYrX7J)ssj?DH!~KKyRZNhe$WzYueEbm=0p7P%RvvEGQ0#_+zi=P6%mufxH~~ zSj{vx@4I^JvL0)&_dZ$qy|ev#U(Ung{M7s$n#pW#PG?grh&Tn=&RU`eamtXk`mVG7 zyCd+3Vl=ePN@^E(HMCpF0mN7GWS8ToAB8Bmg`BtxjxhD1L@&WYT8bj}r&5_}#%zEwgK&=$h`Ti2Im*W~e#ev8sFs07$zz?d=f(s^L_+GCx;uefYUr}7;lZES z^}$P~U?V(Gyi4EyEMcy}6lnbA7P=4&`H-4@n5xIx+A6~>S0W*GZFY1GKdwgXKlhgU z>%iHaM)TU)#a0?JWPw^(GchA4Cp`)ne)J|)ylLT<5nce$E3;Dju@o7L<3tzb+uL-S z{19hZG-WD(;|h%((=)ALTR|hQheg08#**RezkLH1XPsHORIJJ-)qf`E@OPZ_%n-81 z*SxoJCt&ge{=`7VKFBS)Zorm*rSt!4f+~e-w%*nsvdpuNL&ZfVKQ3wzi2+Tz&4!nvU zrsPL^H5!>ssNQ@Q#b|W`XS6#uhvqZQxxv_Uc_EgWX%MfNl>L%mY;?CXJS?5Pwcpcc zY_Rw1;u$;-r}sU4YCrhXU_UpzKBkcou22$9+gSI?T=jmyW}C~&EIwl<)?gi!B2f6( zkRAMtF$U_fE8Tn9Uia0Ll9%4dbWS3626U6SHde;KnZxK%bY?3f5H>W%48IyHJyZcQ zq0tDxlrtz^dLLwn|8QQn+q1l}7C!*(&o=-UDotGdo=M>6%MK0%w;)PLV_s51y>XV; z8v|#Qf$lR0bSG!3q6pS&EweCEArq>n2Zr>P7T!z1*dy*zMFg0uqpW%JEwtup4w zY#>l#Fz(IzsMlGS0mrTsfO}EH2oe$BE`Mkf4!PnvP*^g8Pi!UxhgM&zP5el9yY(_! zS^lABU~NY&PCb_PKA*-=zeb}jShl*f9A@C8FPshM+_n>L5TV^19_SlZohXILT9@FG z>ZhA*N#|@VaHVu%n=E2$-Myv;%l(s#0*SmAZG6}e6}YG6gK>U$(}K3PMMkKI?x*u@ zZd#9!5AnGIpQ6K6Jp~`k#+2f!AltmRHC1Lokv43%7FK#U(L%xaEg>E&Ux2X3C)>DJ z?;n#KV-Cw=9@=?lRQ2AA5>#;FoDqvnFpe%Qr<04#i3}lgd$hu&#@M;#vP?SG8VCw! zTzwT0ccYkec!;wrAXbKx<^JtdE8JWH8rS52jovS(I;^j3PF@MZB-+>b6jsoc7X`^C zp>$?^OgQ#7AAKmg8LOXKFO~~qKjOpY?R&$gKGPYB)2CdcSSig4-2^C&HcUXOS8LLO zMsafLkG*=P9GA+QG*jx^3}+M=_C97cb0_NSoZ27(O%T$n;Lx3>IM%9BwMWF$biXjn9mz2R_Y!it)cB`XJSY5Nd zq}7f&T}V`AISPog>aajs!=6^o6bDs)8wLw`W+5rVb|F`VcZf6hGlQbi>G`qf8yLv%28+ga&5bL`D$E`PvDS!+pUyM%;PV7F_WT*2B#eAjAvBX=c zfM24(OrRUmSvFeXVg@Xvk_IU#r_kCXmbZhBz3?`j6DVbGX)4daI<-u#Lbt=|y{Bt}X; zo#j86gBTt7d#m#Ty+$?_3KE1IIZ+tU@Fitfj|~Oa=f?a80vNLd2AR%~!Kh!G`3*D5 z=#G%Vs%2%3HNX=Mka(>>$zdqMR5+*PF&uuDuM|m?NZ{U({CO^B_A&ip`D`$s!3Gc< zMZN?W-&HhvU^KqO0h(I!b-FMpQbV2<|5z(zX9=M2&t*HAr34My>&&*PrHhnTibEaf@)l7*k zFU~cU^f9cE2p~RPGuX)LI9johjdKjyAoD6p&$UajJjG<=tByzrmCv!zm!DMVWwZU) zZ}{GB_x*wWU+}GN1^{W;{>?oM_x^gB<@Wzd<^JL^e^EIm#{Z;pod1VXxqoAV`WIRI zuk`JYBJNM})8Ah4KhvO?S^qt?SgNc%vLMEYFYuV&jS+{yXpV&Jh0C!o+Z=GHST==L zjTZwRluc>bW!fdufw#vU@QvgL9$q)4sEP>SwwYO57qkU!gX}`{d%n4u{|wKJ|8N+@ z${X@09eWL{H7r4~tA83cfqtt~v<~EK11`w8Ds*U5JAlL(iV{Ia02~E76VAN#;W1iS z0%u(D-Z*vUz`#b&>)t>uxEF~JI#3EUg(*{_18){-h_H-c8Ut??*-Vxs8_KQIm)C*? z5YysPcU7Xpi;8>pOpt0VLMSFUE6+Bo1B}une? zmu(wnJGEB~Vnhb`!Y^EukhA=ptP6KjEQ`QK?G2^KQZCb8z<0kaV;*saZB<`0gqW3DR?7VrM^~HOZTzEN?pIJ)UBqJYdbv30a`RD zf|Jht25dnZPG@OCkqCWwt2kyXSxL!V^U>U+_dUx4bGvPIg_I_ANtM z={4m)?VK$p4cfxHwnJ3SDd4jnzp=d9R!$Frw{|@`HZy;nULMnXBfqQnW+B5X4*@W@O|a8CfQ8J;uh+wu z!JEO^6%K!!?bsA*t9l%8dIoO3SJ|lRSM!bpTL=ai}pG z`#y{qIGvsnOcIWKa!F|V0>oWi7GPA+gTscL#KR>~n9NaB-If;*2Em{8O3X_|FXX|L{37bNsE2{m9kDobk=2b7yl2J!1S_v%IPV zfQ)SaScLj#rt^k%D=S2||cbqUx zxpI_Rq9_Cr(2}+UW`-soyu|T=+crJnj`*)_x_<=Egi2ypRTgLv}K`w^spY%7P~f8c&he zAv`1v%z4{z5Pi&F?mMp`BaSfd!^!K}@Yml@2Isr-eG~L=bZ0Mu5=EmpCW#+hUP8L> zWrWZex5Vb7?HjPeJ3vB*YDb9)?CKla^wmWw6M=e~Dn|ZFFrs;A$u!#w!`yl>!4g^k z`*`F$3E_S!5R{I-wj{ZxMD?k{goaM_T6`I1rz*9>Bv9Ber5?TXRh>!{M6ZBtzm)@f zf8nC_qpkzGx(f`Xu|HU3ek9C3X~d@+QuIJQKFbsaF7R2$$%t@COKn^*BWZ9TA5zEC z8f0okMS_EU7{`{45v^uI;sWfo<)CyqK^Bchq600Y5H2J$DT?iUg{g#q%o|=_S|M}5 zmX&mufGUO46rFQ5y_f03Af-$$~$r=QU~6REl}@QK8+W*E|I7c2oG5P zZCE$66-M9EIspvyj@@}2-Kb#Jjyi&Fg^w`HMcAwjL$4pfgqq2PuF>BzU0Cr;WcOCW z28CdJvnYq$lZs^tfmaN>O1G-WVMrnVj}!pE`AWSBsGb>+^QSJ@AkPCDna%DgWsKiW9&xt}fT|v|==)xKmMnE|vi4Y(X!e9ak z>UtusKT!bV zf@co>;pZ1e-Sk!HGTTw8^8N{)TN{TI=VJ3WnyaAMIYaG zY09^EzPEbIkW)`r_Imso%fUBCO&K(&@Ob~Ul80aX;>4EE0`sj!t4j8!jQD(L`S?$t zPAD1rk25uE@7DkM(#DVPihp_8!}d;+XwB zzPe<1&ks%6FD`oS;p?n1{}{jEzR$0@{-!<~>NbA8@0Qv5-+%CF&zJsq=H?%|O-kQB zb$GDCGv;7SH~ll_j$pjw?cZ*>ednZwLvFeI+(`F_pIx``oA7zgz& z^uAX3c0%<{x18*gbI+hlU-|H7_h%CZ+ftU-JoWv-(VuPU-|b98k1tpLZpX*VjemdH zIAiQW<7V4;MeoeG@A5rG^Mq@3!#pqe)_(oWpC|vLdf89g_K)%Hd&5_FZ@;AB*AMGl z|M_dvw$51mcEQpg{uwluHPrYH8VHxAmj?VYRB_YI%$$5BF^!RQ1ZN zJ8v<6{o<>iJoEX@YtrAH|HY#lpLFcp_Cm*pr+m0-)4z_Fj2!vV$`02TSPr~j+ z6}unxUlwN)*It@=ut%4dM@&9*-?EgK$91Z9%)R=_-S_u-&zS$H-ty5a6S~D;{m|0Y zx?5_x-dyAQJh3Wg!P37jc>npdnO}A|v3bTn#?PC(_|&z<2U34{d&|~I ze@ndTm%KTr@A&-vgpCtUcia7rIcZ_=#NK~ZB#m5h?ASdGuAh%pjEl)kcy3ph4Vgus zZmqli>B6TIhLi+Gb-VbD<0YN##;T1^t?62woijC07xQS$Ki93j$iDuoV>jwcdiTBd zbn3BLH|DH<(YAM-W%su?mWD!?xWC)IaCY}!e#%Rq{?WKUf7tgwMIUb*KJe+w6UMK9 z`X4V|*6HS{C0p+QyY%t?X_H^EteR$&oX0ox zeScr=lkZNB|3?4tfua@j{&vUy3EfA2T=Goz`pdUm^KS0WE0f1ADZ1$AgDF@1pga1n zryjfUbj;i*dsrrlhs-w`e>{9y@$T<0x9&}TY4o~BL*E|resSWbtM2Lg{?Jd$td}hs za8=A3&x+F8bGMDy6?%Hn{#C94hhOWo`{RW-KK6R2vSr)D{~WaE&~qGn6gpC0UAyTS9tZs9@O^GD)~M(n@i%|mZ)-mvuK7k@i9ambiMcMQJ5 zvG%@8&iwtZuM+RGzxnBPJ6@`p-0{PAbN>DP+ixu0f8`(k-M#+A@z;y*+cvEGe>&9u zaMPG|i>@2`@MpK>zB~Ghq<`KuY1ym7o+k{C9Q1!O=<$^MFB-Vx%>iEycy4jw;O++} zRV82j_=lG~^U1Ti@wrpZ4Z6a1>%TUw$lE@5>a(+|w{GnH>gd_K-uY9vFV-X+i(j>B z>~~Ye5q%zfAR%kc>t7AbnYJ@CbM3#o=70L?jv$!ectGff8=lK%SfswJQ3i-%0~ z&HTfXm=C|pc{JWJ)$r5LSB=aaCtB{vFMi~?mnKhsfBT?sp4{B~bALml?;p$0-gU{d z3#)KwvBOV~d_VWHuh;M2m6`uw!?1=qx9v?C^yKFYp8u@(!%I`v-T8A)RiD&zQg|EXE?{=eQ&U3AL)#eh++y)TR@j~&u) zXU@l8IF7w}-S=4&KcDdOQ^$_nb5GXNZK=yXwJa&WDQnuU&FR&X#H}13k;#b=o7LVF ze{T|+EusJKY=k!i{xA82%oQSx0%`&(M!_xj=gdjF4ooHOCda}#X=Br-F1ah0l#w6A zvszI`U1x4zq~Q^2JvUGwRc9K&XQ|OH2mJdU?@2W)GTLFo&ZDw_;SUm~fm^d*uyN?%5sPPEImki%%jXQeNrh|fx2Mgu-8eF>zF z(wAVtX9Y&VgwIO9f)SsUeg*Qb(ytvKm2T0iO~nX4s)NKwr7oJVDRt3;RT-fu=#1*f zE$ERhI)Wv;PLM-5(i}!$Ng22x>I9RDJPYhr6#*EDiZ7kw6?EVXt#U21!w7^bL$Vlj zW_4N!s+5@ny2ZedS%;@Tyif+I1?h7K_fP&VU(jT9h|MD?TgW2q-rxvuY7^%28T} z*{-88Jp`*LHK_B8(>6*YRvq=)kP4(Zb)xM$BUL2`%z#q{Zqz7BS7XO!=Vco7sW`HX zsTAzO9klpHNA99|d6{HZAneKWjX6MdboT)SDDF6mNoe8*e>FOdC=@Oy0r-6Xz zn^^n;UGx|XMFA!Uh@+4i^b~kZ6CJgg3>WJFiwj`VYk;T@LE16sF~4@34!FflbwDd! zb{GKx+)T!YiG&x#WY^OvCo7QrWHsE;tbizb2tt&ad=3nInPN3WFc=|itYh&U%6@K{ zw{Xtl)Unz3&Yqa-xOd*HX4YdgQtUVPBNv1-FqKWEjh}b--E$Vr$(l6>cr>QnJuhr{ z)Y}#QRx{g!Gc4M;#$_*AG;hwLu-;n$&lnZF>*w6NbWZNPSq=(fb8A%Ri!vy|9&J!E zvN%Lh8gFjfNWO=+9UV<+ci#(=u%_L&Bs+J`?7Qy0ch23pOd?XnXr zM8!b!1UHY6PT`)q$ENCn3c-P374a3k3ptJ2pv(mC< z7$LgECe{ajBdUB2sqyed?NmgkD)Tmii;5}}GlHXuO$<;(r%enK(oa;`u@U4is%#Cc z316C;Mn52EDv*st8mhtr1comvTZU8-RhEt%3;3eqz=%;RS5`uBpfy!U7zK=)@+pEH zA~@yyFUc1SRFyzRB05!3V1&pL>CMCR9rV*AlfbV<`ATfk0Q#XfdXs7f^doz*$rtov zRB??fUPPI3kY50gl7kOl(5akbf&79_Y2FCh8aky!L%*O?BcXNr1)a*~8Cbj>oeIhX z;~*x!s6y9{5l|}(??7naiz-8Kd8w!>LIQM0QPqS5J33WWA&3WjQ92du=u}mP1RFY4 zwIRWVPE~bCu%S~`9};W;TUCez8%EG1#cUXXsvZeAJu0gDkzm6JR0T<}VFaq4A=nHM zrpgKttPpdmAQY^Sb4?16)rv0)$p}_6z9=h1u$u5ig~uvFPByiMrj_jihhQNRR3Qw( zf~HlyLa?A|Rb7HC#}~C}h<~H191*v?QQp}A;vv$FD%C^g7?!iC`Y($q9Ypt1k~3N`qZ(g4&jqsjsVFck1B z^%smlRT~9(V2mn15Ws4TDnAgwYQV45chHZ@0t9duqsjsVa2KP>0tBLQsVqPMt1%kY zDFLf7s{BBJGHO(58}}jNi#lrX9q=oqQz-BV3{byd1WmLJz60i^`~tpXR5ftHfDtqi zI+zeRnOY4aXd-klA!8E-g9(9OsnsxoCJHuS1WgodzzCWs7#UN+ujF^YgN!Nxg9RB? z`n5xGRw)(?2<%Cz4|_m0WWZzum3F~az=7mCU?ku<%4f8r(kj>pIF3?|c2u$j-vEnI z%Av@sbP7fRo+6inPk^N;pV7W**;vuOO1Q`Vjg6wd`3H}R0JIh z>S_QP3+gO`id9Ys8dfm~3RV#al2!2sf>qH63RSTOl2j2!3@l|nK#nT-pcoYb(2EKH zG2GNe3~NYTsIa+JdV+DNvN7;@bzMNAkfDPVQEC!lDVGr`A%DW6R9gqIa+yJ=7D0hB zsxtx^*kq_;$w8he=mRHiwp3wqfHgY*^D&m>4oR3`=EzuD#nLoli|1w{!u zl`{R{4$am$Kvv9gh(VADMy9M6L8oF28jqk(HUthvuC93ywx}`}ux^lrx;!j88TYJ_ zZzHxk80z^L`J@-s!Y0&6sgq)j*xIBgtSh>f=;(k2s!~uYhl@beV_9N4!A*cI@MyXM zNt2Lucv2ogHWElnP5wpl&J5@A{vWEZO%gSCkzrorgBGm>WbE6CyVAThMfr_GDuPGE{y~&T0tY zWXS_XUT)VYYjfaM@_TYP(i8}Gvc{%B*313ep~s%#lB`@f_{avLvO-9ok?0-gfZHwm z9u?0rDw|(0g-tzLTr|g#J1;wHyd!sxe%SaC5cn`oVavb>5v^C~(y$pYd&#Uhi}b^O zf77k{-_OlnoV$3|qIo&Fdh^wS;fk=4kkPifq71q;hXEWM278)Ag!dIY2-sXmb6C?H zwls$wKarpDyMPJ;Y6z$T;fDum38*HZ9vo`0`0;p`Ho#OsEIb4-Zr{p(0kh!;Fe% zR5YWa85QBNa+p!kj0Vl9XGT2>>RC|Ff_jj~c%Y&M6|vFcu%My^6)mU;XR5=BidIy# zqCqR_Sy9i5dREl4q8@Z{pa>OhsAxk)8!FmR(T0jPRJ5U@4Ha!@(2ja`)U%_W9rf&} z2O|OxRJ5a_9Tn}U2;v|QR22-U3mPF0)CRjI4^)TTArHI&iorvkAc9bEIr1Q{&{-15 zD|DLZJkg0FGX;tw57G&lED#oXkWR>afxgItAQ+}5d5~AgtO+7{g-jcaSn?pRkeL(6 z)WH}g5Ap_?J($qsLEaz}NDOcCB(IQ3g!)4sG+Xi@uUN<{WLlwikq3E&%q+|r@*tg%$%Q3E9^?%&zwoAz2VK=< zjs-Hw@WhY@d4)_fj4kpYuaKFBVnZIJ6EfM*63K(SLFOCQ9(mA7A#)BTiaf|GcJd0D zcBs$fL0%y<4@xHwbOQ5}Jn#lARPw+Zuwu!BObYZR@*uB}smIM9eS{LP?dv%SIEr6K|&s+6EgE~tB?nIgUmdfG2~&^-Jx&h zmV|XHOl|UWM*#-xueCb1rO>>9DC_w#_lDK#2fXw zio|1i(R63VMI90~$4=47OaSa_egdFB!)=JMQ@aT2n5@Ne z6MzMBJ$`Iu>?Wf;_Mw+l4t!wk#+dH84-~!ZS&?$0Z(VR-d4b314?i_I+JAx>JYInt)RC zoB9~Myf$6#3C2S7?K3_wbz7v&tNSl$4&wY6pz6i|D1?cedmQ3;(A{+Mv1x4L8Y zE_anf@?wU-!#;{r{mGJ70J^}O3%Ddk+|Fw@B=b;(Bs2@bpc;Pe!#PCinJ`` z_2fu(K4(Et%FgzeRUSPs%iXn4apH$j0oPcamS(W2WlL5e`3Io6)lIcA2651=np==x zwGz8UULlGBL-A(i(FR^~9%vIyGTH_w?1}06=$`#&;`X`f0}Tb`jp;sTmGnqRDrsqe29#@o1w_Sg1i2ld}Cp?b*XAYHkRwPT3Q+iyurD*JBU0jc^ zO?YfeJw2Z5H+;n4=Z}}XwmHwSJK5)|@kk4byUnVs z{A~0``}+<|n^d@_blaa|-X0%w`-w9TNPbr!r>g91Y@hD2sSg!p%%0StYll_Z_zBa( zRe=?TABAPe7}BIB!Zum_@UjB9L1Y1y6-s;o-HZ`t%tZ|&*I$(9@RubN`wJ?nLk&4nl`Gh2^#-KPlBe?A z?yJkXf41tQ*E&8j^IhGJoM#ph3W z&spO1xpuiK>b5PIRlI3Uvh-tFVS>-U=k;hh8x?!ekxKoDEh1He&tYM*NC76WA1Pyn zZcNhfpeX9Zd95+Cvc|3*xOYZnm#!m*dRPB`m3Lv*hIw7;&-C?G^c!2UEL(EtRK||l z|8uQR^8JAI7Ya81!z1MciZgst*Zw8do`x)c_50R*M`l8oZS_|?RMaVUn4^oXgZ^So zrbjm*q9coXom)hf2Ga{;2-I2I;`0L(* zxoH2{%Hn|J3DinQl0(TMtHMEj4bBoMp)QX<5DJ8ny^_-< z#c3_0LyH`#h>3ikY?iXfNQIXS4rY<6>@tZ{e;Fy~G+Eekz(;Eahe=V%%xel`emr{T z+Na`sc{?rXf3f5End;1~rAK<7tJo?@GXlDkb>DqZlb5=EtJ`svW4K}Gw;3Og%cv~f z-064e03JOwMvKgj)GBJZw@*oY024&lOL4Sl)m*!ad*e+En|y1M_-QA zUmuA9vKz}`06PNsFO`~&baw#6M|fc>zHlgjaS9GEwjxK(3JTH^CnY+r$;=4ET{Sm# z&Cd1PH=S@#I@H~<@RfzbmT&uRXW_xjd6|<2Y`Jw!&D1mfdpzpRT9>=w+vSr-bbBxJ zz3&&%$6214V+*az}8-K z6Y|Tc5ERglQsKkGj4BQSx(P#m;Za3|DV&9^tVTtc+CCFP&)gh$$tvf$qGdi`d0<(< zogb8zZIiNnb-v)~1%6j;vfCdDc#5Tl`vp&V(c9Z95AJ!a+nTtJQLR?0uCahMgQaZn z%Ft<1C}jc6`v@*hkQ}~zBevf)XGw#PLwInr1d9k;nnpTD^2AY{D;4GZBpJvgwdQ=W z801+p&Hm6;=!n_2)_b;Od0eAa%6HcUq`Z8o++8h|NjX1__B4(R`e2Jxhop5=TqUlA zih!#zy&gWi-RYl?daEXIz2)iTxO*KBZ7b5nCK{s8xQ5JW12Ti^BFr4Ofnm!*X>D=b z8jPVW;G4={2HzUqctWz^X_ShyiC?&V0od1^#pMr^@!{oHE}tE0bP!lH6f=rdN6maq-Go|ots45ORW~g0M0#bH z#GLVYeTKyE?A2eNwQvoNUc)iUlh z#08?!INZS$!R`#jUHC3dD#|(0_`kYOBZrJeM~UK0laZpOEXeW7Oal;497=w@tQ_6PxeP?I? z!Q-Br`kCw0v@VPw& zpdKEz5$y&vw$j2zKzI6DZlF0lwJA0xtO0uHrsW1&-o2tht1UO6wSCpRc{jA-iR3ZS6`S8`n2mhID00b>BUjA`mk~vn0(LSKZXr_w4H(hp z7HuNZMm}m42|I!iHDQNqR2C*O=q(77-%|90e6Wmx5Sl$VyMUU~1?V9>ejyd*oSyj` zj9`!t(=l)&Hi(0hs09e>BivWg(@bA6Oo!PY%|3PIxMcN-JY7FP@j7OZPQK+aQ-uzA~`luoVX@;tmi7 zjMF5bJ|!NQ>TId_f#WiW35KJ$wS6a1i6Rq|3uDIDji^pZxY6&^_4S9`Qo!jsm5xwU zU!7FG?4`GpZqJ5+@9{URy!Vn{Y)7MQQOPHJBVi)vRyde2f7FKxkd%F0 zlwgBFi*X_b9XfPtI}CPZD=lTy)7N=Q4*bW^Xg+k4G~MY)_eghmGDBk@J6n?-{4ONb zRc1(@b2m!`?oyYOuzhRr{QO->(-DJVhfR;C( z)LoVW_sOGckSfE-jP#J z*f9q-ktK@94}Xc8)PgJ_q}!^m)1;E_M|ErO+BSaW$@s?|xRN7rWp%czLOPZ1^&~hw z1tFi`<2(}b|G3w6IQ{U5!|8Rk6}#3HEc6HbYxcbTiFb{zPnW3s6!Ljc7P@BG)I*Og zsK;8HZ`eO>ZbQ)+afA(f%s9FR+E5M+QXf_-e&E&xPEBA_($vbj5R`BZA=|UmpP%6N zd4jHA{<CoM2TdJvpx08SEax%_%E@@j8Pk-Mb0 zpCK=2rne-fR#79l@wh{YV90HL#!Z|`mn4rffRF%pz{ zL4n^iH=zhwSPO88D{dJ%Y*fxrkcwY8o1^Z0(6i{#5kw=HWe}stDC&u18i1C;B8W!R$ZJR0 zkfxz-8k*^^5a=f7D`Ezz6~OhYF7eB&>TA4JhewtN;yo3O)}X7z2OoP7+vN?}bd6We zkfeRib7v21tvuseU9tIi#*AAwExAFrJ}qiEzMKb#g{&F*%4TKs1umy`Vj5_d2N~sL zRt%(VdaqYb)x|&rgSUD573M@Nq(}#O3lG=WJCeOReSzTEG zd51)sKCmw5v1Nd4!zae&B)tqgZT`r%oQIf!s@}VbIV{>txV@Y)o`I;HN6X78^I4eNakRXg z(wPA`VvmJv5~%;Qc5>wm(Ji$=f>qRi+AU~=^0uy>p0YGamcyYg;RC;a&V*fzb}8p-sKCZ03o z+qQRGD_>q)_HJwD!{cMw+EzkR`Km&O*4nRkn;tG>pl!R}ZF-29g@$J%$H&`gN2a(-GX@#$~$svG$(SO(ZICJ}zmqvnA<`4(@c0Zk@h*3;Ch*y&L+ z=0iz{a#|VpYn{hwWt(y$Go^Iv<5PPUxu1#|;_e;ukLxcfZtQnWewRRXg{Nejr_8nT zh|lkkitD6hIF}ud{OjdjDG>1Z{Q=}-4+NbnY%`=BSFY>8lKp4DJ6d?~&Tf-(hF0#G zxjWjabNL!`24)Ta6HQ#AC4kC}Q@+MN6883$qQyul zkb$s0ZeV~@b3$Fq0ZxQ^n+U|GcdSNE9Bu&QwG`BYOrmMlsB9v45L2eBk@W(pFR#bX zt(?zOQ+!<6@v{%Jj~1;S6CdBJ-=J=prEACJY%J_@qB?uBZQ|~suV?Q%cVk0A;5%0x zu3_;=??{r%Sr-fizj(=$5JCb&T+db3XsyZ&lsIEkOZIH{Rh=@sryKr0G-uE1jc4j+ z_d9lNQlWG9?I!#1gs5rfjTERzJdhLQ&C*^L^^_4S6WfMU)fj1hBofO?8=|;b^E?buGtmiHo+bG` zq5e`uMqlM3x&(;XbyIvEauJ;m=}-%Tfht^Xly_%besZjSe$u$8+Nsus0^w>&tI;Q5 zV;srDp3zzvxC5A?irxj>l8DA#mgj*glafSbMCa2ve;@y9zh3cAd^>4OdB=U_hLZ)m zN7X&2--B$yUdiLC_r3evjHSs5f$Z|~ZME^SBX;)Ov!;09m^V@eM^!ywi|}wYLlt)W zb`QP4ePgWTM&!eUItf2z5mLnk=_47 z2{hEojiRwt*bc*HwQ{Vq=SzN!T3J>ig7aFnaUmpmF3`+I`a!pE6MI;qM&h0eaSuzR z`KcM<(|-4`L_teiYIZ5n6R|BhQAB~)N=WXY<|fp)A;Gcj1?MI8*(v)M8re7P=M{=+ zZ@SlYLrQ0<(;h=n_3GazoQw~433y#0U!F7DUl;VcJ<>{Vtq+$#2E1QCP$7A1rSgC; z=*RU^)zas0+|ii6vfrqNWz`<3MB25oR$6wX-=A}&!RMCm+O?}jfA8SFHzYifA9wG) z8Fy{bf9n`iczLHTmqyzQ5(TZx&Us7_kAGX*;}Qj4J0W==*%>Aw(wJb`vn)?~^Z+ax zUeo&Rj>;q(zIeAWxImoz`{O@_%>y=Y(lL zCdQRK@%vL{HG#5x$MZ9s#d~6w`YNjGlgr-AJ~-1K<9)2lrM3l+Ci?@S0B(Ww_&(p` ztZV#p&uCX4h;W**jQ#@I!V+;m2Map6G=QIYt48Kw@@^_z(fp=tcO^IZ)fpHmr#49uB&8;^-EU@S20|g4kyj zjlAPW&jwK=39{wFWQ8$!i-}Fx^$~lHa(|zO7awc^3 z_US>}htgEp%H!&o_387+e;HO3>Jv3UN;LAylAvWZz~_pi#&io$$$zehQjNQyOjfc< z+uB`FCbA)oh#_-7eJ~xP4N6h~%+o7XB)ckn{)8G&-m(g>>++pJzt3A++h`3Psrt0U zRUi#|sIs!|q7I2s_@;)=6TbDW-1Z`?16mu`qDC$%auXppqit^cM>O)91G$Ay3o;V8 zZX=5o9|*1l>L}2>)qp0^#P6*vSCf(+J8i1R?}n=5sl)%(PXSj&a=la`m3Z(}K2-VU*R}DzkUJf=;9d6E$7qWaO0S*4tybIk#!^|zM0j7kRo7WV^ z{CM=vwNJ(O@^)I%|6<4SGu4?}OONzESFu%+W(0I6>%RM-CNFjSR=49S$8f{WZ!?s|C>|asZc+9MWSu|#1Gu7C&Ya_dF04W)oDk@_jt`& zh2Yj5I~r!bbMEZ9x~DgfnX<3*h>poqVh0a!Z}XQ05`16l`UXe2>Ljo8Kw!-9tIuw~ z)H;1ZY|^BUh8Z$uM!hRbq{md4$;QbR9%wLbLiU7|$zNPL5%pMwiG=C3dI_8wrgz1j z*G|t&#nP6f)AM6pU#3hLmryMET+RYleKMH1t03qQa)g|Rs6{kiv zWP6-n-`Ftv%o%`s?zYXtUvS<~PgT!yN>G*C@E2Wgy1{5G|^GB*ht#ngb8Yeh6jo$_xC~@4; zdAWru;jLa~sVLzorSFt9mfDj#-M%{J;L3#B{`J9H5~QCWY#bT%IlXmKcFlc<(*u5Z zGJGDmHv9>2Zg^Y)_fy_Cr0Ci>)gD`5s~01E{EwgY|H?)wuXO2y%WmEC(kA3tX&ts&D$e4 zcf=gtRwX& zr0P5SZguWInC}XCJRwg>0GIiC&Xw(RRZG(s2I5Z!tFLlwE0gABI!ZUW_aA&Cc3PK9 z6LZdG8P>k%>vm*ad~kPKZjqxRw5#5|*%Ms2zslE`Ue%Z`W%~kwP^hou|6p?Zvz`1K z;(G5Lxa`4RFAQHa>zcJ=%1XzSukju;)E8#eZA_Xs@*(G*)h*ck;F+9*DtrP|S*#J>0NJ{gA=ZRdE2~o&vS^gMiZJa4G^`V7;@oNlE2n*-5&E|! zMcqs;pl(%BH!BxVr<5L$))l!kL&=1B!^cwM3z6eRl$(14@J#!C;O0mA?eBAELsW+} zO$WiC@&bq*rMG}HV7JfMQMmm$wxH<>Y$J(>M6FCppfZl~$o6NOOgtpgEJDoUY}>J; zTB(%UBDb_?aQ${`zi6aJx=?ncwQ?x6_egqCtnblJ9$p+=-zEzGiv;>00oPOW5J z(pU?!E(uj@x^ax;^e}#JWy{v693Sp{$CVk|H?Nsg(estFGsZY?s0=PYc|dwol0t>l zFNVjdbV>3!13{0^9dI688L0QV^7EGMO2|4A^&GHBcc>WHqS=gX+f7gX&kUsP!*fmj zm<(hW!e+Wg=BRcb!=;rr={L2>pqcGRYeO3xl5j_sS8D8m{9kg)%d6^Zyj6!smIvZJ z6$r_3mH7NlSI{p>4cU}cR?d*5ea>@d4{WVG<62#@`FO^RTQ)7ZLAO5b^{6gI%^Xbp z!AJvf&yWBlfE5vrqyg4%BNz#~G^dNS>rEkzq)|qVWU=sR+x0MyF_GarG8*%EA??S| z<0q`B(byq?ZU?Q29%rE1lX7B(9N*wnbQ(h1YQ`-!gmlP9BOd%^i=nG+{L0E9-pWJn z(q|m{>glCNid>t&FV7G7OF-!UvV=gHK1}OG!n=N8q0>*-EHe+v_>YF3&kU*mBXfO`=qrp z*R=PrcC8#ZS`rMd0{oXM5SFgE4d|6BsrbS%CF(aYw-$h=x((72CnY+r$;=4ET{Sm# z&Cd1PH=S@#I@H~<@RfzbmT&uRXW_xjd6|<2Y`Jw!&D1mfdpzpRT9>=w+vSr-bbBxJ z<}q~#@oaZ8*kVRl0FgOSuwX!SIQ>QT8iz3Ye}7wtb= zSsai&fm-QEaws`u4bX#)0Uz?IW1?Lide$-7i&+&rOGoA>Mmxc3(#qV_Zk&3i^Uc=O zR-Ae!t!z#0#HnY}%G1<#w!TanNtuN3I3~g*)qjxXbFyYH&RsOev5lKwpHn_p4}UIN#(!?)^3dHp8G)2%bpb}C;HX}_mvm;oi$Rhq%plZl#C;s zE>9@9Bj~?9eTl2G_`u~$dw$gC$1xx0IBqxGz6{d80`eaTdB5(M?EAVfYNBovJ#QGP ztzIZenM_(Ki=x>gZSrPxlUA}KjZNuPKusNmU{Z?1knT8q9-5>2fI=8H;TzWIf~0ZB@E?aBCr&gIVXaWRG6m+#s6_%MgJbIj3AYuCi4 zd1Jgi9(A}2yT|rRi|>}Qr!q6Xd(2xWokzFb_m#sX1f+&xbIr_WN$40hs82`@7k1QUsLj4VT`Mm=&kD7+q`GHk>wiYyaM zBW~jGGAJ@ul2|0X4E3zUZVWF&Jv&Jr6kcX97|kTXLu8p?5ixU-Wkxem zHY3W6CVH$sqRfCR<}~UN%`ChgftN(thnHc@7Lo>sDKoRUBCru=8d1+^GjOmOO=gmk zB(fgR!i>d$ENf(|BeI@NAb#wKGSnjxpb=$ui;2YIgqI0ukm%J+naJW4^~@xvKU2>_ z4hk@3HWIMTl-b!i7Yra(u3wXpXc&?2noQ)NF{@X5qaEO%sj3v zSiD?&8u+tVdHh*4@Mqy@RWMlWWOFqf4lFe**_L3+Owb_NGAoHTimWHvN$#b{GQmzf zzL903jm3*Wv{=|RWWbR}A}b=_1zMPX1%uVVu2ax7O#l}8u1(BQMb@(k{Cc+0EDB6Ljj`DHxipD3a$-OHUBGMS))nfpb1s0jlXQKN z@0v953^Hf&4Az9OFy>vbM6S$aX8D@IWHE8P-DCyR=Gx=s2kLQa!(_LTJ^x5Jtn@OF z$TGW?<#h(LQRLT)-OjC3)FaALM0;kloy3MjmKn_~A2OgGIo=aq54cAsk!1$6%|iC? z!s{8Zn#t8ck!3ar*2pr%od_H)SRmy2cWo>!Kmb5Ph-}Yb5lkGtSuk@Aad_7i{87yWiai%csnR&dJtsFnKK$YaiVz#rofC2UR zGOLvzpT$b)R1T(Q~nKBDImuSz*@-R}y;Zy(#^6lAKeH`!FNoagzzsRA<&XE9>kgo?q;@;); zFra{?51_@y!;2Kn99|%fxVgl#V#Z>y3e+zj4hKX11s|gY4Ny+I?m`Ds0ZbY9g7VL0$XN>0v%aqz?*CzSbrv#hoK&46N9Ab zCQsyiKw0MdwQ+V7f|J;>fb@A9guk6zFGd5;hrl*ieMm48rV|M-%Ao8;mchGWW_c%i zz>?wXv3wHZgTod2uyFj@2$ErFU%(wDWb-5fe?}v}u8c-ANBhv^Iorhu6M|i*Ml2zA zZJ>->Yer%qv11WoOmby5juwot?YVwUBDW@uCJW0aVRYMAo+uD^4%4r}X5@7^n}N55 zP>;vG*~rnV5xj}fo6s+(OQFopjs;>y;N+muBC>p(NMw$e7{P~_btT}+drpsl+l-|d zgUw=N{VE2SjhrnBRhb`)70jKTORJUaF-NXHnCuMh!4OG|Bva2ueXo)4Vv4vvFlfF# zJ8Pc{aR2drKo#Z2XE(7nHr|Dkf*l{!CXU`x$%K4S)VH2<<}olHM2I3fCR^!9|`Gk&FmV71{=7eCRQ%sT?=cwpv=avD~rI6Sr9Ee?*s-oe=Ip0W`KfY?8oP%HjY{D3*>)7AS>WJrkz`n=BSi&jG6VGT1#F?yX?jY#&x`9}JN3 zIvgY@iJpnX9~3rj4MBn7U<2RaV1pUYwP!bQHalj6hXXnT2OD$-4$p9bvh)k{mwgw8 z3!HrnY-ZxE=gN3~WhNFESC8{u3uagc3|_F-?5r(GI1{Tgka{d$K;(A5J=h-%Es%O_ z8O8zu$Br3$J6sv!WjOp9xqV3pJQKU$gEA|}W6jV@7#s*@C`lZip&)Smf>hZx1TQ9M zcbmbUxjw*$xObsS@nu%dcW4GNusRn^4UV^)%_Nd6l0L9nIQy*d!dh78{EztZ^vDk>fL9^>MZ{>TxuKos;l>a~C=0-8*NI?%HehX;bGdpF;u~ x^l4MFvvc*BHT@KQ+Qh88vr!6~(ofOR=sL%uTpIlVcM*1MbVG(rxcNHW{{d4%{kZ@D literal 0 HcmV?d00001 diff --git a/public/novnc/docs/rfbproto-3.8.pdf b/public/novnc/docs/rfbproto-3.8.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8f0730fb7f947147e53d2a3eadd453408ab0ff05 GIT binary patch literal 143840 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

    GwFe>b=0IJITgXgI_UKOkWb^@Z0S0GRXwSX7R& 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%0DXNY31vj)5}Qz|8@U=jss3>TK|jt|J2{p zGSoS#--j1cmpX601p1kWxC%!Y}hg_D(yfwQ^JPwJP|@1nQL z7B(hMOhR_n#+Q5bxBOkonnW$E!M6d3T7y@(3!8vDXabNnu{CoxCuQXV_sPlG(Zs+8 z$vs&oLJ+N)6kTK|*`Gk54WAP=sdiDwXb@Q*7a#kY&|>qr)@%IPnJ3<+M_nYtH@-BD z3Xd{*et#_-=E5^G^C2%XoJ&ItKYgrVOJPps1xmno9K6)h`_5JZO5as_+SmGs->E6d zbXsZqE8}J-MLH*k^xz8iEjB7@p=Rzbc0sX`av)|jf2Vu*fYy45Jd73!f%eS~tB0Iy zJwldc*i9XJv)B>tVwWEcd_N*}oFCLfcgSe>oQ%gA|7S&}%W>WXb_$AWDzXv~NnvIA ztEikVC;%wisn}YafCCyF2+mJ7m$>$qLB7{tIqF z`s;UpIsboa|5aX3?4%;#Z{^{V8U3HiD+|EJ!q&wJ2?(%pv39nwxAyo&fRR|)0fsKt z)+WwKU}S7f%nY0@>};K^4V*5^Hbk$^6z5`mgoVX~<&?qWqj<&e{KH^?IDd`?2kZH0 zka9sPo-Cvwa2?>}kA?Jfd?09pF+LfizyAIF=6p=R*IYsvO#D|>{u`t(XCxQ>laP>B zlsJ(;8Hp=82r02%Ep6=FKRXD{IWKw$1WVevv_blICOMqoDk>MGE`8byHn!6v2PEL+ zI+^lA;()NoLdpTLk>Gwq>doi9K0BVE1YSnPcJc>ClO0?_Wdld_Y~b1n8#wr72iMU- z5H(>3J4hh7GR+0%hy#o(8y7pc4u8@mHgHbF1}<)az=;MsI35SLkaB{n_>j*hbU4^A z;S1KfllbKyB=}Nz^FNhfFq}qqHZ}%GU}5>IU|y;qUyR!&55xIavvIahCWMv$T1E4%%R?*sx&EdZw0;7JxvfP;&jvx%`G*z%Y=?i% z2w-A!a=7GWFP9cB*ay#$I9h<6<)vZ$8%|HFTz|#zuCj)P=3R)UbNwzc|6t0vxh`os z*hrkmC2Ww{tiNa{STex+%XV3w{;Peuyh`e#*FR$m3Gn2Ph0*2i{~Wo~>9xNV*x#Pc zIlmI{+sIuSh0E_?`U^k*$jj-J(BF&=@NzlkS7Y%TFaIt@m$-$P%*)bwqWM3=PRNMM z8rYcp9-9h*A38jJ<`8U7PfXuQJZ1)dDi8@E=wx*A$RYSk@Y8|@_TnZdqI-IJLi(hQ z1qpD9hM=vPHTW?_B!H5$iH#~LCpY+VS2KvPfJ0a=B!G;8`{gqWCnsVN=<$5-)lFqa#qlSxLM_zILjz0*g4DK+B+1k_sI1~GvyB| z*>{0YU>0druS+t?GSN%PKx?ffr)kM+Mrme!N=l2;jEPrAF*d_CgZ*yC0{#aGVsQn} z`~rbYv-`cuJ5LQ({YezR;oZyfRl`gf2Id+#a<+wf4B#;5CG~L$-Z2zxIoR0!$Q~ip zmC6O)9LziXA=bdMCQn`B@|Cio4aW70j{RRVwIrI)P4gCaAeUKuv)diwtc7#sNRvAo;J{jf|`wp?7Jd1 z{;Lc9JL&yBUOp3Frjvyy9N-xcup>X0V73bh27h^3f+45p5`1#y-w5$Z;RSG|T%N^~ zr*Y@csPmWD@>kh~IG4+^d&#Z;ciH6tU6x%cxpk>q4unU(Zap$Y1Mn0jB;$1AqsSeY z-BST5^jSCQ;L1c$Z%>%p)~4(ltyWI**{#)a_fD?4O4v$@+cgV#jxq%4?vVs2%ddM< z_}HZ=(`?pz-0@EB{pL-4Jy9A;$o#8&8_A9ifA-{IelK0e_v>4CC7VNxLvSn0@AG?rHM zoYu@+WQiIp*`mz`mMdH2vSIxWJlp=Me8@Y(xn}dYJfa?%(aww*fdqV*_iS3BT<87499qfo)VG|dpZ9qJ6j7~eD08Frd&gGNT2kUZ(mPT3R9e+M;5kxxfH3AEZaNR5zDtWwoZ!D~BB2j>C48JNlLym+PLk+lImfj<&NqDSM03$b?% z?PT&%zfY`ggzI!2WCF(+Z9q;DP^tLJl>nFup<$tY3J1q_%>(GqDr)l=5Y z7$}@Y2D$9P1ZNzO=wck{w@~a@5bL}$WEF83Ow$y2sEC3k{WT`4Hf%E!hVmfB zzcY(^LCCY~3vpVNaYgx#H>`Kz9P($A4~svH>E+oTAgpf$;`Qhg-X?IP7WQztA?i5t zjpfCV;ozo;N7o$#!bjO$DEnPSxzf)nMi7*h)G#~O`9fNhAKtQ7`EHtL^Qog_)SA22 zj>Z(eJ0p#6gU4nsj$CNnPC^#HF#Mf8?lrU?JXU?!&RZGfK=THRycX zN(Bh0@j@|;!5vRqrJIQy)&Sz&$=+X$egsx9h92=C|rp?Y3Ir{AJKM;eE3l|NU3(y8F-jM3CSHc>xJrAmUCb?Hx#NwmWO-Gi&jjV_*I!hFJ-6_~f`WRpiJc=yM zmh(5KhMnMOGhQmqS>|^w@08g6N($5VBqAi_zhIo&8Ub8MG#p!qT3a% zmJ+-srL{mMfVsOrlGiB^X)W5*cltH-$j2rU#n(5d+be^HQzk!#Cht8ku#4n%cP}!A zbBl-xeMsAD81S?;yJ7SIn`>l;`f;!Ko}i?)p{b=1_Iknm9XviRG?Mc8coMBjT1f)c z{>|nM>M32`JU6xSPN`+&=4kAX4j(GmLA1w5&84udhqVS%eOg0hwt+e^O{@8Y-S;@{ zA`I9_QcWu&vV|YDrPkVeuOsM1Y+`tg8Pj(>Y`MmasjXDi7=+PbLW)uj3dV<7t`fm$hDuwNV5RD;39C))8;WNT+_xUUq@X~I57i)B&^#%W%MJ`nk#e!m%z@cUHjs(&2D9p6c$ zNd0+3wEo3DY%D0$XQg^yk%)7RmlA4?Q$WRC}B0S?e>&t#% zRUOL7bRWH*80>S6{c+nPt}egQa%koF=@S1C`{Q}Wo-Xb0eMX?CcbKC2FyAaDQv+#FvY+~4YM>JguaX9=Z01jdeZML0u!XY-?y2?icWZ#vhS+4(qsAj{!rI?axyL- z+R7HM!V?D0k}>pBasu)2epWkh5$zI4AR55ZG<%C%7-$aw?&=F%2uWyp`OZdO?D2G+oD(uTaVs!Dy)cNd!fIWh@shHag zTHj(i;QM$zw=u17^5gN#0si|Lz6eIi7+1y4Cnc@_lKK5!>w`F7Ch%H6CLq}Ta)C>6 z|G&Cl5bz(}?_Gc}Kms5QkOjyClmMy#1ArmG=(G$5Fb06VF~AI92A%;pnIbR;SOBa5 z)&Of0Cntanz!qQ!um?B-z<&6oiVJWCxBy%MZUA?H2f*`JUkpjdFZ<$4>q`H7+zl=n za$fSq+3JYSN+z!w$>f>a=P|OECbX0ZjbGnPc_N73N8e7J6jR}>7xkXXW{lEiY-O2$ zKb1o!(OGU$^qS--J!Y^qEKK;jKxDpaDIVVjGxe5MyIgz(lODsKeqJ&>(9D;jl~!TXV6gms?-X3Y9q zR5BuR(i9_%!{tNcw~#|x-cBTI3pN+Ev|-FMsk87;*BlfX+KOrlHYc(P+N$~{Lh+Br zJ}+Z=kVJ*fpSm$oqJv0axK1lE5~&iUq8%5@y*C>4a91STzi1|a{b7Q}+iP%weC3+j zkqWv`cO4BChLJ~?gPkNDuT6wyGjwhS)V+@q-d#ja(|P%nbRJ_ou%ZRI#nq7LEBRNi znVW{U%R-+@Fw)H5BnXNRR13svp*E!Yia;LpRK}WG^_Es3d&{>LZ$pN!z$fC;)HydB z$;=I@Q{h-sua{Rw)VPKEH#}V>t}|YVpA~kHYrWRmOMEZ<^AkNedzpH>nNi-j2?v8H z+szlzJ94|TgLOH+EH=9@8z<@Nm)+cYYwGsb?AAP%h`--fdu38QDlEP0Djkw=za5>S zIaoH-x5vRXagx)|+C|_s1uUwaADX-K9X`7r1VyTA&$o&#Df)ck1g5ow~r&rqbp)% zgX)Et6y!a_9OG4sW(tD`MR*h9bF8TH0VPn5A=dXkKp!u!eppCU``U)kF;!nu>{OnR zfK-Dl&77IvD#<>%Vqt^ue)@5}A-5h?2FNs!^EKLL$G|Sl&ddFzr2K;J_cnME-nOcO zK;>pxr`=Ti4gcO*$J%;yQVJ35n)_;HHp@pV9~ytm7_pTkG=nC~BO}A&4!iZg<84<& z6Q&3=L7~!Z(-qvV2v1J|Ci3}I!h02F4oz7ij|vSxf4gqZs#!pBNOBL!xD=FBWlG>A zCqiG0j+;eF;pY5#dvZl6f=3&vuS1ZgU%q*+K5TrWMWw|4!HX%p4A?`jeq3DA-rQ}< zp7Baad0C6_T(KxJJmCaKhig@iMCKj+Yhh#rbN49RpWkXvuM1YoiB_@6`8rak`zAYc z+v+yiaG{!f6RIm48Bds!pI2#7QRKoqL6Nw@P8cSc@3$YjIyb!Ld))%M0RqjZsBcku z{x~e<|4TlK456_RP?GKCXugiSe(urydy+z8f^pb_!dw7M~ zZ#gELJ^N9Z{6Y35o3rTaAD;KjzT|QWAv6ii*nX!^(S7!8vnpOCMgUQ~kjht~ny($! z(PDGf1pmd@BAbQ{HIMzLfPQEfboqy|w=p=`gk+ZvgcaTnr@-Ge9o~$0Ml6O>pzD z()-?8Hcok2y>*vdjUQEDw*2h9J2UFhb@<~8+59t&FM9QUGySlELJ^A6k@@pkIl@zS z1_J{tDYmAo;0u!c`1)&oY;rc96~#KQ?Wt9F-0vm2D=FG5GU}yq+wOVh)H5nVis}MN zC(J!ZNeg+a^xzntXM5=qRJqR06`|S_Dt#|+%_JapvCUU?WjaL^yYsq_Y_^iFiZ6d6 z3P8L5*lO$`WlBk(Tmog|1DXe)@9~?8%uhGMz1%@WNb<&60$do}qr~74;Li zcJ^c+LaVaEzdk}H+`8YS1D~BfAcewXy0Wpih{BIAKa3v=qkVYjAoP~DQC;qVf4D+0 zUnbu=o=&}{DSCd9rkK`aJlBEMzWj02tRZB<_-5M74@og(*Zn8W+3WhZ%-=YN;BTpN z15xjBP3nRZ9*7vSyNF zAEVtse@#3eMObpeQYi#4adEP*#EQUXi0N@>vA&zvKO4Va0YLG=0X)G zXR=ngiX8di(k64DRRhCQ(zP=w1@0i31&Cm#DJePGxQJ!FT|+3( zz2i2?Lk!YQ7ID&>=y?b4C^DpygeYw)ck-h7x7E*R1F#>dBQTR4+j3}z&~q`_zjdx& zGc5d0`J??%qmIi)3FN_{|0h_-9p29yu8?IH_Ps?Jf1Y2XJT z)sYeBMQYq88A%xH|Ik_e#* zrAUgDY@t3+HGp`=nN%Ns9gSPW8OEcA`E^~Ar6m_JQS{pJlBE0#Sz-`Boa(j#EYeFA} z_bEsA+dciZ(YyLnREL*tk9|^D_ph_EC*)kiUkDBrbu3u&>Vt-JYUZyIuUPzNJuk5$ zZ7l7Y;jy-cx-%wK5|&zh?zX}>lclB4Ze+`AJX2N9DYuKz?5b09%PTfmxNFN3t-cXW ztKPh;SusDJ6?!+yY-LHY&8Y>RF>cPHir34Pvmc1#1%?#|{b-wCN8R){$Z|e*_=A4u z%Nh69jl6#Habn^)(-`$h%dMv**)@+Up9;^MjNg%K%y`u}BYiwG z%qiMQ;o#<|nkc@*EAKxh*f(WwaPikZKQ5OZ9o=|iQ3kERVYc+TcLlb zkQ4fwYWN>o9&Z;e3FMU3m!+=SUU2lx1j;9`YiIIuJ=CT}E-iWOGcsLucAxo3Z~mxd z_fkD`BF*eymX*BkDsy^9%Wu(+zT1=)l-gvqvr(1%`vvOO>DxMMXPAECFnd?uo*c5?Xy@UGH*eonryup%RlgQ<5l0QtQgHLLJG#j5 z%nr8jsSWj@)9e|@AQ}M z*=34a{^JyKY|INxb~)7!Jz~?=WV}D4OS4K>aQ)4b*0P9Uogn=gXhFtf=dlmRdAa^GMz3ik?G0n@-$1;dZuV&Q&L#JGZ>K-S^5yC+^ve!+6c9 zUmnLr>M?SjF0?$A-T5KY!|`x;ZBdAs&ykjk7v3fF$Gb<5o~M5#@r7{su1m{{w>dmw zcl5H@WfywF*^Fe>n>Nvfrrk;L-%cJ{u+nCb@{8BhtlkHD8z#@W^S1o6)g)fU#1#B| z9^G5)otjtiFlCeCQ`#}-z@us@CMse6%b)g`F3ydK{6xu4x_+ZV`Ehu1+W2^_Or7%u zmA`GTb!u*|oNd~DKf<)@)7ug!eo(He>Ol*ayv$pTN9*sMV~K7gytscO z?8}Be{M@^UwY#&=Bo%1ipEdhlTA_AuO{k%6mp;=ukfb69BbRUV__R(Xi!{f#T1)m+xy<5VWUb+bMrfcl`8b13jw|CLCMd#d*-C)>4W zWlz%L9(*5o;i6VwgI?)FYW~~!r6$dr1859ye(iX(%eP+ z8Xh`9%+F zf-WR8meCaO@rzC`(wtlWG-G)4oZ~NqN88?*I|f9QUY{VzIdve|G$n`q#lp~Ps>{q7 zP8lx)7H-ovAK!hU`!W0L+K|JED?G!@nv{gK-|nQuy%SdL>3vRFRaSiR)bWY!4+_Ke zrdO54syvGI=$N=p*>pO4f=;c!UJ2MBZ_+|Evsv5QZ=k4-J?oO?vwY=7vGQQ39{;J#jLZil<9dWax z@fgm?!kT&_10ek?@kGt>+>RhUd3LE8NCy}nmzkt>4gok zqt_i0FPQJZ;&r@zsN5=P{ZMuhQ>iT1o8Yg|;2!ZLJmrLQYjhp+o>TuhX64OqPsV;8 zsnMv#a`?t*aGZWTo4!K+W2(5ca**id>rV$ytk)9htsY_gVP!)?LfxC8)gPum_fDk_ z(Jtz})%e6!?rl*0valJ^@0ZTMQ4>`Ain(O??7S9nnc@vfP0)Vx1!ukYjGM}ukBe@M zYp-h_E>V`O?`u*&c=65NYucKb!43WfPA1!{GcV`!vUY{6;h1_fwVz4Qws+iXc*(Q# z`Xx$hB|HkO>st@I3bo@*;8+_qsuW_*@*)uTz5_no+Gt-bVQTs9{>I;0_9;nBW%L*Ab? z(&+0UKcpJ)i~1)QWhCdS|2(7u&oFTN|My%Z(DUgZE)w8_CFLUV{g?EUs{!PvkD_I- zy9Q#DrLVgX@c(}F#9&E3W|no47$)l?k^jnCFHl9rhpiIIR=N7PJuTtPtl`mBulog> z=g!q$`J-iWM$4{c_HNsDwfoEAe-M8)7l~wd?r)&C_?47QInwxcp zyw>q6%yfD?oznt8V#Q-;3@I32G(MxCFoqg&<0e&?I*;nmaed)DYW;$l3P%`{qp>}V ziSXH>INfV&KdcW3W_Y|h8`#x% zuj4>)uz%};ZDl!2xkAyh_I)Lyr@YNX8-AvRg!<#k0+}z>{;NY4!#lO2de-|KUf|3`PYM0{C|6{%EkZdy{jH)N2Uyu7>u@?YhkZ_~Oa$#?xWsB7|INo-eF z_xz!f5rZWohLm*W{@TeL-HEAm&iqAgu)Mt00(Wy^$f|({yA)mX=@gvHWmC+8!Rg({ zOF*#&w|5@+zQd0oilfjcfG1w1PcaJ$qBtNIs89;fuk%FVJl_Gu=EC5RFu0|r*n*R` zNFXOCH%QY7p{-!sC>Y-dOU!Fv9OhZP9N?j#>5|MaK zP^f3DnFV20HYZAwYal5A4k zoVaaWVo*}DSlux(EID?gWR>dL9g^+Ul7#gMu|5f6b%WlHxBy9%WVPzFdiw{RDxG6H zH9EE07F?UE{SzT!ed-sUpMQI~O$jJpOwuX>r z*LTVY=_TO@o5bI~W3IL~KODz^zVUCfXk;20;^h;_6H~l-egOhwo%ZUpI+OrkW1YoJ zTg*1ZoaY}fceRM;xZ2LiXLXX|NPz2K*#x2b17*?&pJiHrD-GAjXg?JF@cIA`dkRwQ zo8|(1dbSBrpmqW!dKK#LXUR31VMm1o2;h2v%j`Ml^ADTj>N+_cW{sFoe&;;xRJH*mtZh75)wo#hVog;eGPkGTFzDifo> zk_kml(A_4^fVLSPmW0xq!w!3;(;Vj3eFYJI87{;Souwf*l65FCugsaJ@tt)Yd` zCoCBJX69M^!%^@rc9gHrU&Raw69o+nbYCADFNlXq6bgNXLpgnY4EaJ4n0uINe>W)& z#+Z(Y5&TmeAP(Z03``@lAb2!{8a5~ZO$);yFM*%2P9)Wr$M=HJ6CLQ9fz-a=n?ESP z`fQb_2U;6xeg8sIA;J|LI6M^g+DcVVD+xcUj>i&~y zIvw^SXhMMbKvP23!Vt|I9H$##Tm}Os4J3eh^}qb@FGin85swd%B6KE037MJi9}0`f zgm+uw8fXja4s8#2FRqt=`2K&B#oi(h$=`HgVQ!@Jap^`ILtP=vE=IyQ!gn`AL?FDB}McwrO+ zFOv=^rBLvWGzOl@7zziM#lXXk*nU6@Ma&Crz(5o^(s`kexrhKo8pALK15pUbU^oLu zv0Bm?bO#r)X%jFyV#<-lFvd^XzyJ|c$)p1vh^0#!gVBN`ZXH>S!zR@OxB^j7lXN<$ zFB8R1%3wH*XJY%|95&*vkxmCa#K8y{l853Yq|+e`5pv03z~zicY@{(Hk2qd1I+H_c z19(pyih+>Mi_@7*6wNP#F)$XQRFJ`7;*rNav=yOEVOn!hT%>GXXaZ6?41!9C=`a?0 z<%CRLptmN2x5E zgMy%m7-GDT!Ego_@heGVNFE}@lED~sHX@Oc#$Yb6iFrXUWG1G=xQNe6CNB_gBkC_{ z3}!DIMPA5a5PL_!a5}sk;3pVqe&7(hN6?Eg3>b(MOI8PKfImkX106WJ#vx+J!Bz$X z3&A9eD-6Q@h_((e6ecK}7pOJKwgg5hq0Bfiw-DzHl82(Vq|1Uqya`b+XrQB4eMqMR zj6qyKKnEMKRY|8q@<=w3!C@nQC+T!Z9?6aa2IpMabU4gGQlEn}7^(fZ9Fh%#mLks? z5J)I94kYA=dr7){7=ufi3mB~OMEeKzU=quRFhryxEe~{%)-B8cII0L`!C+({B1DfwHhj?F>amp}x}ckY3XIVZZ`|FPjeJv44V*Y!1|yiTJr> zb+9;OjKd|(L7-$L*&M(~dj@D`a+x_?(wGGII79*|T^6VZmu$0PGevAa9F{HOoMGY| z#EmE|4+D326f!N1K~xkB88SxNBf)A;9QPoPv?gOrHkW9dKnITAh)U24old}T@_50R z9E>!NSai}F4>};siy#j$QXkP7MpbD~T=*aomWlf{rc6yPqM4lp)p z?ZMI2iEKIyqx`AmYt~2ObhIn4Uaqd8r zyKK9F$sx2Kuvi-q*JO}~Bl>-rynvC;8nCq>&IOPMy9PpDba6!NOOQui8)29d>x;v- ziCAWkM`{;1a1qa(pp`*9BO;7=4h3m&2|y?_Eazm53kHUe4onltCc>=>=?no!V$wMo zn1o33;2w!I4&Vt1`P>NmHPXCf!AeLdA2bW8k6_6o%`LdQA=z;T8?GAumM0c@0R?~v zUQtV-IR>l-CM1}R@M>fsywDh)Z#z(E^8|b$1zHll7;c^g#ly(Vk^?J*6`O5|?uN{) u%o%gAIV=vG_h3XY7-lB_cZwy5jXYHBB@z!@C&DOX(HTlwT9yl~l>P^6P~6!7 literal 0 HcmV?d00001 diff --git a/public/novnc/favicon.ico b/public/novnc/favicon.ico new file mode 120000 index 00000000..45399c8c --- /dev/null +++ b/public/novnc/favicon.ico @@ -0,0 +1 @@ +images/favicon.ico \ No newline at end of file diff --git a/public/novnc/images/alt.png b/public/novnc/images/alt.png new file mode 100644 index 0000000000000000000000000000000000000000..d42af7b421b9f1193620cb4a7114f1df27886d1c GIT binary patch literal 339 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)y9AGfn3c|@SfJ1YPZ!4!kK=DA?d4@Mvg-UsUjQv!4@3E(23Sx$<4+2dh>^&G&f67BAwO*^{~A)gQHlw4ThU?9=P7U#iY$ zoU@gULH+&Szpr+?q{tjL>$J>$WYr(E@cJVsj=AZ~_m3?|T-1=LlQ4TyhX?0_FWA34~+4uP000>X1^@s6#OZ}&0005JNklLQsoytXHG%qsDu&24?kb6(gJMZNIUjQci;%GE_^nL#i=bVU0+3)v%yWQ@~-6FHu z?1ydJziiuX0zj=+gYWxKhGG0P3X&k2nr5MT4Y|wp8MrATn@2jALL#zx6ke_*NzM*Q(LB=qxlTR{ z-;+rcMIa&o#WNuyL{XG4;(6ZLfhv-T5CWd(<%>9ubKQ3ik|dGupG+on52n*8WzD<# rPah746vwf&3n2ssgTZ}w#D9YiP?b1W?a7*e00000NkvXXu0mjf;r`$= literal 0 HcmV?d00001 diff --git a/public/novnc/images/connect.png b/public/novnc/images/connect.png new file mode 100644 index 0000000000000000000000000000000000000000..79e71adb85cbdd2da5b59d9e2c38609dd526d1be GIT binary patch literal 404 zcmV;F0c-w=P)P000>X1^@s6#OZ}&00047NklS9D+ncfe;N96$KSXNI`*e04{(81y?{Ei6R9Of(lMRK|`ICPyi+&eiUAZ zm9zdypa3aP8p(P)``(OKvkOZEAgLANt&n8Es+ql)h-}pOd;)L4*vyoZaUk(X(h2YZ zD6n1y173kMNkcPx$w|M`b>JGf1FjY{IRJKnLo*x0Y$q|00GDQVGp9jbXaej5Yrr!= z6v_iSix@a`oW`_)kjbxOY4Z0?^0`QVpqcM2o%j@J13gJ2vf(nCK`weuV_Im@L}d5R zDRAuZbw>Wp5CivAbYhOP!dl(+VukP6=Za^N0FPdB9#J3{kN0zN8cC>vtYm2u*aKRw zgA~{TE`S}r|8)dB0q4NbGfRO7GaHt4T@FcoWtg~-r2gDO6$V{N1I=O}sav-{^k{{! y4cr3b0^fFPm1r~rl2*vhD1?cbjhg8Ee|!N&_KPMesy;aY0000?EKa`a+$6maEEblBXv zqublI^Z5ssChmi=1rK>`n#>Cpc5LscXyxJQ&70LIB0IC{X&RH3tZ?$3uhC)uAAQ%~ z=dm#0#6w#pC2J)m-^*`i-jt|dOLJh#dSJNyOLkI^a{|+uvu}bz`JFkM*S+>@X!rHq z_o7j{y!=7c)kEp~_RqX&X~247rqA8G!CuF+|LYjD=OyU#_5bp}sYKbLh*2~7a?){X-J literal 0 HcmV?d00001 diff --git a/public/novnc/images/ctrlaltdel.png b/public/novnc/images/ctrlaltdel.png new file mode 100644 index 0000000000000000000000000000000000000000..31922e53242fe85fb1db641a15339e2c33670e5b GIT binary patch literal 317 zcmV-D0mA-?P)P000>X1^@s6#OZ}&00035NklEIGHbhH+fhROmqGP}QpED4br)*+f` z_?l_{ec#LO+c)rEu-peF-2wwZk-8P|U}oc72a=w3m0px=;itmLe1 z#^)NUsv0@xQqw6Qf3i;eeYVpf={%ANX10{n1-g;U+RRoliQfnwftyI?tt`uHNl#|h z+ZT|ukaT5ci zMJI${Kqr++6=)XZJ#Ys&cHgZ7UcC3Sx~_*sQC#x%+c;nbytZig#9=-FEZMBb(?pXA P00000NkvXXu0mjfvjB`6 literal 0 HcmV?d00001 diff --git a/public/novnc/images/disconnect.png b/public/novnc/images/disconnect.png new file mode 100644 index 0000000000000000000000000000000000000000..8832f5ea7e2bdde7016b9429917fd453e42aadb8 GIT binary patch literal 1378 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 literal 0 HcmV?d00001 diff --git a/public/novnc/images/esc.png b/public/novnc/images/esc.png new file mode 100644 index 0000000000000000000000000000000000000000..ece5f7cbef684a41f4a7bb9d632e4cb2310d6b5d GIT binary patch literal 385 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)y9AG<$XUxK0|o|0K2I0N5Rc<;r)=~(;vmpwt~763 z(rf03YMC%Gv!>p;`x=s-3jg3Sv+J08Hk7^Wk+j5=)(kI8VZ)Z0e~+b2TwvK-mAbF2 z{@d)ElGk?5n>TO1$DHDGXL8aMJSYA45_?{oD9s()th9+WhB^2Fuf*zCdB(y{77Tew z&kS1%{uE|Re6Mm~@64I2-aW6}r=)cIK&W#;RvKH+>Qzztr;B)Z{7}kVtXRpGB`8_- zo?X|~X2#5`rF$<%rB3&A;!r=3y(X+NI`nR)$B{ESr#C&d_Xb*BZ7(n}k>jDxsd>+C zaTaJQEjIC;?vf&+TE00gNaUzi!N1irSGT2d zB?5_@acqyryU7ALyz3h9c@D1yz$pMUXc)psjw1kIZjC@DSJ!pvuZF$S5EEVD`sO-K zwz~vXl@a^8vuXCdXQVt?^|DBBs};`<&ZW>?Y2J;;?7QO=uH&8|a7?raW54z=y-+UK zVvcb1_^ff#m~1{1q4bC`vi{NXgX{e4l9BSnCkyj_O)k$bT-NXTtaW?GeaRR8=>9={ zI559(gopow&Y>}1=uA{k`xoEyx2KvRChM%TiVOhwVpN6^_0yNId8osL^rTNZQIULc_^3E@(-y%Spc5Q~vy(d8L!)(x1n*7)_*losOPO6XtxtI&bNw>fdLNZ8Dwf z$IIAo{quy`M$-j;DK3=%w?ZjAaObK&H{HAptRzKWIeQ7*KiU)h_)g-r{Sm_V_d7i@ ze&ymaw?pTfHml?IIZTe*dmMBUR5LfIWL5K>i#m4b+qJBy1&05xz4K!eX8XL_`Om8f zVbx5BqIT4GSlSgy?m1KRiH|?#SMyHA8TzN@`>bi|&VIbRcH2a6i~WBVl#% zWRl#M`RQ-2?~glB>Kmrt@$!yDk*4p)b3T2KBbe4c{Fv#s_FrD!=E(K=v)<*pOqy(B slDTM#cEa+F0UAq7I2|-YE9TfUc1w106`8X%0>hZW)78&qol`;+0Ig`bxBvhE literal 0 HcmV?d00001 diff --git a/public/novnc/images/fullscreen.png b/public/novnc/images/fullscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..f4fa0ce8321de23929618a8d82ec0e42c094bafc GIT binary patch literal 851 zcmV-Z1FZasP)P000>X1^@s6#OZ}&00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-va^(3nu(M(YOEr0^mtR zK~zY`-Bv+s;y@IRx`*0@QVPXFFCKc8$+u~>Y$TrSyqG#dTNQ4bJ8V_B9{+$SK*ata|d z_Hu^3hQr~oH=E7W&xp)sGj%u|_Et8$s*dA0jYi{w=lQUY?Yo2L`EaArxNscD@p5vM zQfiu}NeChCjb^j?a~-L7yxPBk5JF7TG%2N&!x&>pk`fTY%yc?6m^Vy_=lRVKJlJFc z?A&xZH6Vl;Ns&A=C`x)X8a;YExk{O$k%5NkG@mu~yfX!NKkN}5v2 zI1g81E=f|tEo9ra4Iw=76M+z(*tTuE1tdvIFfV<>GF@`}7|ghHQ56<>}p_Yd&@ zhXp-|WrMKKuzF~*fjfK@hf8Rc)~>HVk7T%W}$3K$hi{VHgu;tEy^C5QJ^7NSNpOD4XPRxs7Z# zd!0-s(`=0p^1N6qUi?04u~@tiLY~vTG|`Fws00I=8X zcJJKPwBPUd%H?u$T`wt@%gKJf-*cbqcDr`~fW3S^zt!nkoc>D>vAZXilU$7{O2ZO;t_j0ZzBO&B@KA%6i*Mb(y`F#H5ehBaWNU(Scg0S-^SuF^{ dj`v;}eggqm-~P^IymkNp002ovPDHLkV1kszf;IpE literal 0 HcmV?d00001 diff --git a/public/novnc/images/keyboard.png b/public/novnc/images/keyboard.png new file mode 100644 index 0000000000000000000000000000000000000000..f797952513b39cc75d0f0dd4d6716608e10217ed GIT binary patch literal 1283 zcmV+e1^oJnP)X1^@s6sPETi000EaNklRNjzKsl+3H2fX*Se?>)crik)8OqXL!g(Zk)QUl+u@eZ{N?8!v_!lURzreuTz#~)o$Ou{ncK!u58uobz4={SGnacq(;4w(VW_$&Tn0? zLI}}g46&zAPcHz$ULt<47-LVhIeLJBh>%XF$y!}QAP_(_7UAhsnk>u0Krn#eSd^ug z(`1!b(Ld0S;X^TADCEg3n(+F)h>wnk)A;$yRWL9yH#>)^$thg9d>IhH?CdP2Ca3Y! z+$BH=%+FuP)Z`TAlCx;HT3FcjvlqAg;`IegPfj5@GlR{|O)M-fVtR5KKVFzYty=4~ ztKG0fWOsT+R-mdX6o&#p2t`()soJ)OkYyQ)Cd1*7006QqL)A1WiV6n6AuCW-4T`LS zfkAdSplT`{vJ3_WiE(J^mfypX-^W&bH_NSCXW|FATe*W}`{>?%qSaUdQqGj>F}4;qINgs8wq? z^3GB8_xJJpckYp8Sr|P$hDapBQ%_PN7+mkKYV`y^?C#TK0kuNAd)jltXu2&tkJ-U z58kKbOcIrH1t&i_iO8AL;_9U4fg~KzHkK$PNeru;=_E_bY=I=X`e&0s?}Fa+y5r zwvA|P7!KKiTsDVhqk(8N3e~A{qhL_AR>R<62)ak-s}+kXoMV4j#+H5vY>}A`x{LY_0?>}|w)ThJ4!xj6} t2m;U`qQbdz=l(i-_H6Z4&i6*0`Va9a7RbS?<+lI;002ovPDHLkV1f&1X|4bO literal 0 HcmV?d00001 diff --git a/public/novnc/images/mouse_left.png b/public/novnc/images/mouse_left.png new file mode 100644 index 0000000000000000000000000000000000000000..1de7a486c76ff1b95efd7e5ee36dd6943cc6dd3a GIT binary patch literal 511 zcmVX1^@s6HR9gx0005TNkly zKhD}f5XIlxu*TB5qC!GGMv9cV1WC&YKqwXxk_(^!S^NKxY&N7`ufy{^BuRp5wTe=yggB0|-EL8<)l{q1 zx;aUA_riL;R!Ne;vMfj`A*BRk49#Zq*`4x_>nA1i%`P>6%qjo?002ovPDHLkV1jL+ B<81%{ literal 0 HcmV?d00001 diff --git a/public/novnc/images/mouse_middle.png b/public/novnc/images/mouse_middle.png new file mode 100644 index 0000000000000000000000000000000000000000..81fbd9bd375b2daa88b6bac6a7972c58b708a67b GIT binary patch literal 517 zcmV+g0{Z=lP)X1^@s6HR9gx0005ZNkly zzltJ35XQfr(H;kS;J`?bd5jA#-1w4>%zXkyP6VL?HBbltU?dpHCifEe1osR^UUJ;X z1cRfFO!r0W*)ulkvb)@VSrz^Ls=KHz;G>D86P$Co+wG9g=OLv;9LE3v%d!9faU4Sk z0ZJ)M)5PI$kc1H8c^=V%XS3OlAP62f=cn9oIK=l~k4o{cpPta^bWZVPGJ%v5jYi|d zGtLd0&F0HF6aW}wnP(OX1tqT6>xiOAoaYoUrBrnQux&d7zLc{pOJOO25CUo4noE*I zi4|}h#~H_VyUjqND9Ysa`@OyurfFsjK@i*orj#nNG8Nr!Hwl7(FvbWWnMwR-A_P)O z06>x?5`bikq19@gZk9VY9FIqgMk75@QwIR)^?LeNJm2CTJ%6CI*SK`}9;JKxYG9q- z4g6|_(xBgO0(KndX)qYP2xQx~epfV`&2PT%-wkcI+i+d?rxpwVuIv8sJnv@cVzB_F zbX%!ZK3_dN9*>iLzb~(8I-N?+d2)%N{l4@0T&z|rQZAR_I1a)vM6p;zE|-Jv`&h5n zD3waGR;yi{q^o;jxm?OH3}F}sgb)xyfH8(@wR-PP`Oozit@F&YG*W3V00000NkvXX Hu0mjf=bh+1 literal 0 HcmV?d00001 diff --git a/public/novnc/images/mouse_none.png b/public/novnc/images/mouse_none.png new file mode 100644 index 0000000000000000000000000000000000000000..93dbf5780777973578edaf7ae35f65ed01e5718e GIT binary patch literal 497 zcmVX1^@s6HR9gx0005FNkly z!HVKQ5QhIwv}2%i@+3$eV-E8o-hBc+tO#O*=)n*Y#FOAj_Br+m_8B~RS~ikHFlwZ` z_b|+0CpM$Y>@fS^{-W!vqNpz5e-kMOIOpnkJfdE&Ln(!#C;$MC;{X7PqJWeVlv3EX zjq~}e2q7d4!wTz7%jNRNWHM3L3W7kDH6D*OAIGtZqDWn{tZTF1@1Hs6S~130rGZAH zq4|Elk37$%5lku7GXS`*TLIsSIgX>TG$W-%S@*_-5SrJ(MNw2T4u?YplIM9Pb~>HR zqp)qel8~n9U0_P7=Cu`>PNyPGQ^FV{gqXL75CWxMmOu!h04T;7hQr}oXSM5u%jJS7 zip)eq9ROrDo0&)P+QlP!{Xpeh>(=8(RG#gJflc}}@P`#DgMR%Ju;+Q-gCO`Tkn6hU zTQL|6z9mWWFmyB;!T0^2Mlb;QzW+N6!@HsD^%|7YL#NYudiQZ0$6`L8R}Oqxi^W25 z&c!W;@%ye;E4kfnNxR*K=XuDo49#W}wOS2Hl3=&nq19@sUaxm^lis}xo6SaLSq96p nAf<$q5{xl)yWK}`$`{uk{?qW@S6u}n00000NkvXXu0mjfItA$@ literal 0 HcmV?d00001 diff --git a/public/novnc/images/mouse_right.png b/public/novnc/images/mouse_right.png new file mode 100644 index 0000000000000000000000000000000000000000..355b25dc9a03e6d8dc4b956dc51185b5ff8f63e9 GIT binary patch literal 513 zcmV+c0{;DpP)X1^@s6HR9gx0005VNkly zv8ozD6o&uV)may2D=R@3@)EbWW$N@+xA6%C$s!2bXrV5OSP52gU*WcSf;@wyunJk3 zz~ZVSJ9DdRoLvVs+=S%s&SC!V%pA@f;C~azC&DmPyWI}uav4e~2q6Ffj^h9TgbH|GwsANd6d{B}QACX3*=+WObN(?5!&7cF8X*XRd~v+|d^*KXA6}r8Lc85Q@vL*h zX0v%bhXMd&tnkcArJ}{p-##HtQ+b}#z?4$m0l;9)~m zJX}3InM|_5V4$vPI-RO846{oNJ>(2+?q>Rf`4Fzff;%=K%nq(i6F}{53M#?;U zLBZzq!>RXNprLZ*hUsB!Gewso}{}^ zyS5zqsqt+q$3eS=bG3syL}aJxswlO({ZMz^<8@I+aHiGz+=bUm8NM^#j@;<7{?FO# zrb}1(?6I-+nz%_ndc~vpE9T2SE8eKOh+S~6fqC~wzI*9n-{OAGRx&S`su`qNAN+(V z&&XBkX?|VBYQH4q-F|DjwzF)~vb(lB%jUA~j<{R9onkH8Tw^2Zl+T|DXQ^pge?vdW h@Q(gJk3Tz4NXMlvaGP>8%>o!=44$rjF6*2UngBV_o1g#y literal 0 HcmV?d00001 diff --git a/public/novnc/images/screen_320x460.png b/public/novnc/images/screen_320x460.png new file mode 100644 index 0000000000000000000000000000000000000000..172ec555c304fdcd2cd34c3f920af2cce1009406 GIT binary patch literal 12778 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 literal 0 HcmV?d00001 diff --git a/public/novnc/images/screen_57x57.png b/public/novnc/images/screen_57x57.png new file mode 100644 index 0000000000000000000000000000000000000000..e2085f29fc2ca0147251b8327d06081670943f0c GIT binary patch literal 1807 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 literal 0 HcmV?d00001 diff --git a/public/novnc/images/settings.png b/public/novnc/images/settings.png new file mode 100644 index 0000000000000000000000000000000000000000..a43f5e100b3dc9b5aa30812521b036e085b44a39 GIT binary patch literal 2495 zcmZWq2{ap67mm@f1(k}e3~4G-RY|Rth{RgU#IA}GOAR0Knplb(*-?ZwKD}lgz1@N<{mC zgt7iq{!H2pO9THaZ)1wiuP9-HDzwN87W5^b67wj}f|6FgVcZV{$DmL``DtHF2@-Q8d zh$%I5K+clPszf)Il|p}A@vJD$n9C{WNhDL*ayESt3FAtrNO}?%Fwlnclx;Su=g?hr zKNRRh8TTkZ0;x3fJ~1eF7Rl@^SJfjh1?8tcPLH$#DO@0bJe%9UxMr7LaZkHdTg!Fz z!=hd0p?QtY^^Kp;t~EBlA4$6<_(2C-q&j%TaJPALJtSc^JbiioiDL4=+J$tq^T^0d z&x7O7JDB!3ehcH66a~_W$uwh8Z#J=`!hU^z>uFf@9NwVdygn1Vh1Wlc3n9Y%=mI8(BcN1nS zEePZnwlVha!*sbf9kX`AfW4X-pJ2ZnaS?4STPA!y87N{KE7P9U`tFa{{2(ZQ#% zrMZqhT???c__y<<`qqSX<1X2)n;Ir=oEBy4y9ub3Kka~U0cOlRciJ8KUNE=mmB-X} zgHSml%67OZC-EzY}Wq@|xUU!dVO-_)Phm|uA*BXtD6VUSWHDOXk5 zQW@ner1m7A7+_V+um$kLs5V)vKi*mbT?0nMJyMTPE%iBn(^gG{mMu!i619A6F^s6t zPhI4%bsY5IQanRZt0(Dm@?W3NBhH)?Ajm!I3q{D}X@$)QS40p_%}b3aVRGxD)AUD( zVd1yP(_7%tX$G?7L{c#~*8WCV*!-$w%g7#4cprIH!m5EeBsC=|^yfPO=u z_x*z~5(@bSVO%tTx>(ymOlTowh&DnSaTnlwsRxA~|A*v%Jf`GOVhDxKpwNOK2fW@s zv@nJN6naqT`}#*GiTJ$^JuD>fTNM%!Ne(1a$w7=zqy|C*`JH?}7egYVL&JOm$VA3B z-QVfHHDK)Rhx}I?9Ukd+e|66pwf2;@w`F)mHky)IW5dd(g$I{fu$wG|d ze9__p6WkgfAMeb09B}J?dhyshtD+}vXUM0e9zf;!?ae{B&)zxhEoBoisbXBO1aw(x zl*GMvTX_S=d^jd{A8$~k#v&fHc)T}DPR%wg=#JtcHbZEq@Y^?@@9p%W_V&W>U55SA z#A#Rvpi(2J=jXq??(U9%IX+GZHZp4a`f@ara5j$gw!dF0pk})#DJiKmI5@b4Kp+Tb zHLG3D^a=}Ghle!m6jxNx<31D>7Mg8;{_I{@os%Q~;p4}WS=N*H0|OrXi@AW<*QQRYsojc=jg3?QgV&x94`)Xbi3K4M5j#eB{HqJOxw)QjW#!*4*0??F^CwA5 zON(A=-u=D2yxa|k!%Oz#1|QV&Q{*^0IE>%4P(BuDGkIG$vn{WW0DGDD z<(w>>;B_?i2aDQK-)hxZ?e@O&qA!N7L-o4)I!+on&f4Jd#cw+6GOfspzWpOk>Z9$l z4Vjsl>&IS4eC5r~dUSVpZym9x&(8QO$;oXAq8jR2TlYi_m@x`CnPjPYm4_uIRyNkw zkqS~$VX;m4%$ml=#_HPI4M6oA2(#IBx3Q?t-+^u>4Fa{rYkGKiRNon7Zf|enF@31) ziXp0(*W|-cO^Q22aAR(c0_=*Dvf`KxX@N7Qg9lj>L+#T%psklIsw{7K)eV@tgTFql zxu=^-@3?*YwymG9Z_?w!wqW7H^7(}Y#?&}CSqeWTk!e+!n3A%={jh}-I`?*e+7rea z_@vQ>1{5wu<)|8u$8*jYr+HOnFMRqG6{ivzb7&%ZW%Si6yuIYrJ;9^(;gOM%A;G~K z>YAEW5=`n}D@&(yMPV@5-rSV?^e+)&ChVLX{WlR40s;bOO|1CqGcz(6bp-{)=OEB! zTiae>{#7Qwgq7X?J9Kh%T$Q7vqHSAFz{OQ`JcOcKRDYZCFw}&Ux3om%I>yw`#|z|n zUlKHYQJnLb55uSVnr?6NtOE5cX-q_q{|&zGO$v7#sNWPVA1%QlSze#%Y5)KL literal 0 HcmV?d00001 diff --git a/public/novnc/images/tab.png b/public/novnc/images/tab.png new file mode 100644 index 0000000000000000000000000000000000000000..84134872a881a627f745a432a3c951254cda26cb GIT binary patch literal 387 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)y9AGr(VqkNfCexMc)B=-cpQH_#nA7tgMe!?hZ5_{ zMsAl!Od$nK=NL4coJ!_2I4fwan^WPez236Q zxV~)Pmbreb!~RE$eG$IAmU)pz*R|Ls(Q=zi{&XKq&Aq}nY2Dc~42<~)YG*C`o4+Gi zfzhrp-BY~xz4ptrjS(^y&#NZ29sIEF7IV+@&p+#?rS3j(MVx!X=$#3b5uMxna)02 zw(86+ea1O6Bj-JL&z$wG@ph=1;^e&(PHLnW`jei;9RARTq fhprAg`;}G9rs2;o<>%Ld!NuU|>gTe~DWM4f3=5y^ literal 0 HcmV?d00001 diff --git a/public/novnc/images/toggleextrakeys.png b/public/novnc/images/toggleextrakeys.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8e0a70d3eb7b5f4462f808ab987053fa4b0e56 GIT binary patch literal 735 zcmV<50wDc~P)P000>X1^@s6#OZ}&00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-3J*792=g7&T0Sv0&Gb{ zK~zY`y;U)56hRdJ-ppi!CtQjM#~p&_LBP(=X7C5tSO^h>^eIGa36M2V8-Xh|B|c z0YCsW^8Y^p1OTrA>=4nxq$KMA7Do8L0eDPA`$bOYTou6Q5WwB?-B01PwB;o+fp z@6A9JPGh|HW^;3MYIb&(lu~2>JOprWOfs+X(zO7*G{&IWY)S}WI)qTwS_1&I)`+60 zs=k~97!Nf5{J2{SKSYZsk! zq|T{@Q;fA%^7^l4S+>${x24zX0RVcv9@_1;WLdVNwO;Mx$$S4qYyEJ{)3QZGpGKVQ zol@%Q+}s@P?d=IOf3Vg*D|!~HBfiQIZhFw~y}v0UHRs&Y_V#w_oLlObbksbl7bJR= zW!a-Biry2^oe}TWL6&6?c6WEL1K79Lh9b~<$*uY=B349xlweX3xmDIsHAW{M0OxBN zM#hm`x?LGbnNFs>_XJ?74D&DZq%~Aq4WMI;iJAF}QtEy|Z#01TB4*CS_6MPNtqWrX RMSuVR002ovPDHLkV1j-yMWX-! literal 0 HcmV?d00001 diff --git a/public/novnc/include/Orbitron700.ttf b/public/novnc/include/Orbitron700.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e28729dc56b0662b5d2506ddfde5a368784f5755 GIT binary patch literal 38580 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 YaN=?Mu0Qj~2_>(2Mry;V1-x;lBPyVI3Q(tVP1 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 aYg+X)x}OQee;ks?lK=p?0sn=t1dRYj;d3$o literal 0 HcmV?d00001 diff --git a/public/novnc/include/base.css b/public/novnc/include/base.css new file mode 100644 index 00000000..8d88c0f5 --- /dev/null +++ b/public/novnc/include/base.css @@ -0,0 +1,527 @@ +/* + * noVNC base CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2016 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +body { + margin:0; + padding:0; + font-family: Helvetica; + /*Background image with light grey curve.*/ + background-color:#494949; + background-repeat:no-repeat; + background-position:right bottom; + height:100%; +} + +html { + height:100%; +} + +#noVNC_controls ul { + list-style: none; + margin: 0px; + padding: 0px; +} +#noVNC_controls li { + padding-bottom:8px; +} + +#noVNC_setting_host { + width:150px; +} +#noVNC_setting_port { + width: 80px; +} +#noVNC_setting_password { + width: 150px; +} +#noVNC_setting_encrypt { +} +#noVNC_setting_path { + width: 100px; +} +#noVNC_connect_button { + width: 110px; + float:right; +} + +#noVNC_buttons { + white-space: nowrap; +} + +#noVNC_view_drag_button { + display: none; +} +#noVNC_sendCtrlAltDel_button { + display: none; +} +#noVNC_fullscreen_button { + display: none; +} +#noVNC_xvp_buttons { + display: none; +} +#noVNC_mobile_buttons { + display: none; +} + +#noVNC_extra_keys { + display: inline; + list-style-type: none; + padding: 0px; + margin: 0px; + position: relative; +} + +.noVNC_buttons_left { + float: left; + z-index: 1; + position: relative; +} + +.noVNC_buttons_right { + float:right; + right: 0px; + z-index: 2; + position: absolute; +} + +#noVNC_status { + font-size: 12px; + padding-top: 4px; + height:32px; + text-align: center; + font-weight: bold; + color: #fff; +} + +#noVNC_settings_menu { + margin: 3px; + text-align: left; +} +#noVNC_settings_menu ul { + list-style: none; + margin: 0px; + padding: 0px; +} + +#noVNC_settings_apply { + float:right; +} + +#noVNC_container { + display: table; + width:100%; + height:100%; + background-color:#313131; + border-bottom-right-radius: 800px 600px; + /*border-top-left-radius: 800px 600px;*/ +} + +#noVNC_screen { + display: none; + position: absolute; + margin: 0px; + padding: 0px; + bottom: 0px; + top: 36px; /* the height of the control bar */ + left: 0px; + right: 0px; + width: auto; + height: auto; +} + +/* Do not set width/height for VNC_canvas or incorrect + * scaling will occur. Canvas size depends on remote VNC + * settings and noVNC settings. */ +#noVNC_canvas { + position: absolute; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; +} + +#VNC_clipboard_clear_button { + float:right; +} +#VNC_clipboard_text { + font-size: 11px; +} + +#noVNC_clipboard_clear_button { + float:right; +} + +/*Bubble contents divs*/ +#noVNC_settings { + display:none; + margin-top:73px; + right:20px; + position:fixed; +} + +#noVNC_controls { + display:none; + margin-top:73px; + right:12px; + position:fixed; +} +#noVNC_controls.top:after { + right:15px; +} + +#noVNC_description { + display:none; + position:fixed; + + margin-top:73px; + right:20px; + left:20px; + padding:15px; + color:#000; + background:#eee; /* default background for browsers without gradient support */ + + border:2px solid #E0E0E0; + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; +} + +#noVNC_popup_status { + display:none; + position: fixed; + z-index: 1; + + margin:15px; + margin-top:60px; + padding:15px; + width:auto; + + text-align:center; + font-weight:bold; + word-wrap:break-word; + color:#fff; + background:rgba(0,0,0,0.65); + + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; +} + +#noVNC_xvp { + display:none; + margin-top:73px; + right:30px; + position:fixed; +} +#noVNC_xvp.top:after { + right:125px; +} + +#noVNC_clipboard { + display:none; + margin-top:73px; + right:30px; + position:fixed; +} +#noVNC_clipboard.top:after { + right:85px; +} + +#noVNC_keyboardinput { + width:1px; + height:1px; + background-color:#fff; + color:#fff; + border:0; + position: relative; + left: -40px; + z-index: -1; + ime-mode: disabled; +} + +/* + * Advanced Styling + */ + +.noVNC_status_normal { + background: #b2bdcd; /* Old browsers */ + background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ +} + +/* Control bar */ +#noVNC_control_bar { + position:fixed; + + display:block; + height:36px; + left:0; + top:0; + width:100%; + z-index:200; +} + +.noVNC_status_button { + padding: 4px 4px; + vertical-align: middle; + border:1px solid #869dbc; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + background: #b2bdcd; /* Old browsers */ + background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#b2bdcd', endColorstr='#6e84a3',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ + /*box-shadow:inset 0.4px 0.4px 0.4px #000000;*/ +} + +.noVNC_status_button_selected { + padding: 4px 4px; + vertical-align: middle; + border:1px solid #4366a9; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + background: #779ced; /* Old browsers */ + background: -moz-linear-gradient(top, #779ced 0%, #3970e0 49%, #2160dd 51%, #2463df 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#779ced), color-stop(49%,#3970e0), color-stop(51%,#2160dd), color-stop(100%,#2463df)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#779ced', endColorstr='#2463df',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #779ced 0%,#3970e0 49%,#2160dd 51%,#2463df 100%); /* W3C */ + /*box-shadow:inset 0.4px 0.4px 0.4px #000000;*/ +} + +.noVNC_status_button:disabled { + opacity: 0.4; +} + + +/*Settings Bubble*/ +.triangle-right { + position:relative; + padding:15px; + margin:1em 0 3em; + color:#fff; + background:#fff; /* default background for browsers without gradient support */ + /* css3 */ + /*background:-webkit-gradient(linear, 0 0, 0 100%, from(#2e88c4), to(#075698)); + background:-moz-linear-gradient(#2e88c4, #075698); + background:-o-linear-gradient(#2e88c4, #075698); + background:linear-gradient(#2e88c4, #075698);*/ + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; + color:#000; + border:2px solid #E0E0E0; +} + +.triangle-right.top:after { + border-color: transparent #E0E0E0; + border-width: 20px 20px 0 0; + bottom: auto; + left: auto; + right: 50px; + top: -20px; +} + +.triangle-right:after { + content:""; + position:absolute; + bottom:-20px; /* value = - border-top-width - border-bottom-width */ + left:50px; /* controls horizontal position */ + border-width:20px 0 0 20px; /* vary these values to change the angle of the vertex */ + border-style:solid; + border-color:#E0E0E0 transparent; + /* reduce the damage in FF3.0 */ + display:block; + width:0; +} + +.triangle-right.top:after { + top:-40px; /* value = - border-top-width - border-bottom-width */ + right:50px; /* controls horizontal position */ + bottom:auto; + left:auto; + border-width:40px 40px 0 0; /* vary these values to change the angle of the vertex */ + border-color:transparent #E0E0E0; +} + +/*Default noVNC logo.*/ +/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */ +@font-face { + font-family: 'Orbitron'; + font-style: normal; + font-weight: 700; + src: local('?'), url('Orbitron700.woff') format('woff'), + url('Orbitron700.ttf') format('truetype'); +} + +#noVNC_logo { + margin-top: 170px; + margin-left: 10px; + color:yellow; + text-align:left; + font-family: 'Orbitron', 'OrbitronTTF', sans-serif; + line-height:90%; + text-shadow: + 5px 5px 0 #000, + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; +} + + +#noVNC_logo span{ + color:green; +} + +/* ---------------------------------------- + * Media sizing + * ---------------------------------------- + */ + + +.noVNC_status_button { + font-size: 12px; +} + +#noVNC_clipboard_text { + width: 500px; +} + +#noVNC_logo { + font-size: 180px; +} + +.noVNC_buttons_left { + padding-left: 10px; +} + +.noVNC_buttons_right { + padding-right: 10px; +} + +#noVNC_status { + z-index: 0; + position: absolute; + width: 100%; + margin-left: 0px; +} + +#noVNC_toggleExtraKeys_button { display: none; } +#noVNC_toggleCtrl_button { display: inline; } +#noVNC_toggleAlt_button { display: inline; } +#noVNC_sendTab_button { display: inline; } +#noVNC_sendEsc_button { display: inline; } + +/* left-align the status text on lower resolutions */ +@media screen and (max-width: 800px){ + #noVNC_status { + z-index: 1; + position: relative; + width: auto; + float: left; + margin-left: 4px; + } +} + +@media screen and (max-width: 640px){ + #noVNC_clipboard_text { + width: 410px; + } + #noVNC_logo { + font-size: 150px; + } + .noVNC_status_button { + font-size: 10px; + } + .noVNC_buttons_left { + padding-left: 0px; + } + .noVNC_buttons_right { + padding-right: 0px; + } + /* collapse the extra keys on lower resolutions */ + #noVNC_toggleExtraKeys_button { + display: inline; + } + #noVNC_toggleCtrl_button { + display: none; + position: absolute; + top: 30px; + left: 0px; + } + #noVNC_toggleAlt_button { + display: none; + position: absolute; + top: 65px; + left: 0px; + } + #noVNC_sendTab_button { + display: none; + position: absolute; + top: 100px; + left: 0px; + } + #noVNC_sendEsc_button { + display: none; + position: absolute; + top: 135px; + left: 0px; + } +} + +@media screen and (min-width: 321px) and (max-width: 480px) { + #noVNC_clipboard_text { + width: 250px; + } + #noVNC_logo { + font-size: 110px; + } +} + +@media screen and (max-width: 320px) { + .noVNC_status_button { + font-size: 9px; + } + #noVNC_clipboard_text { + width: 220px; + } + #noVNC_logo { + font-size: 90px; + } +} diff --git a/public/novnc/include/base64.js b/public/novnc/include/base64.js new file mode 100644 index 00000000..651fbadc --- /dev/null +++ b/public/novnc/include/base64.js @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js + +/*jslint white: false */ +/*global console */ + +var Base64 = { + /* Convert data (an array of integers) to a Base64 string. */ + toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), + base64Pad : '=', + + encode: function (data) { + "use strict"; + var result = ''; + var toBase64Table = Base64.toBase64Table; + var length = data.length; + var lengthpad = (length % 3); + // Convert every three bytes to 4 ascii characters. + + for (var i = 0; i < (length - 2); i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + var j = 0; + if (lengthpad === 2) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; + result += toBase64Table[(data[j + 1] & 0x0f) << 2]; + result += toBase64Table[64]; + } else if (lengthpad === 1) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[(data[j] & 0x03) << 4]; + result += toBase64Table[64]; + result += toBase64Table[64]; + } + + return result; + }, + + /* Convert Base64 data to a string */ + /* jshint -W013 */ + toBinaryTable : [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 + ], + /* jshint +W013 */ + + decode: function (data, offset) { + "use strict"; + offset = typeof(offset) !== 'undefined' ? offset : 0; + var toBinaryTable = Base64.toBinaryTable; + var base64Pad = Base64.base64Pad; + var result, result_length; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + var data_length = data.indexOf('=') - offset; + + if (data_length < 0) { data_length = data.length - offset; } + + /* Every four characters is 3 resulting numbers */ + result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); + result = new Array(result_length); + + // Convert one by one. + for (var idx = 0, i = offset; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data.charAt(i) === base64Pad); + // Skip illegal characters and whitespace + if (c === -1) { + console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + err = new Error('Corrupted base64 string'); + err.name = 'Base64-Error'; + throw err; + } + + return result; + } +}; /* End of Base64 namespace */ diff --git a/public/novnc/include/black.css b/public/novnc/include/black.css new file mode 100644 index 00000000..5c4558dc --- /dev/null +++ b/public/novnc/include/black.css @@ -0,0 +1,71 @@ +/* + * noVNC black CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +#noVNC_keyboardinput { + background-color:#000; +} + +.noVNC_status_normal { + background: #4c4c4c; /* Old browsers */ + background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} +.noVNC_status_error { + background: #f04040; /* Old browsers */ + background: -moz-linear-gradient(top, #f04040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} +.noVNC_status_warn { + background: #f0f040; /* Old browsers */ + background: -moz-linear-gradient(top, #f0f040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + background: linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} + +.triangle-right { + border:2px solid #fff; + background:#000; + color:#fff; +} + +.noVNC_status_button { + font-size: 12px; + vertical-align: middle; + border:1px solid #4c4c4c; + + background: #4c4c4c; /* Old browsers */ + background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ +} + +.noVNC_status_button_selected { + background: #9dd53a; /* Old browsers */ + background: -moz-linear-gradient(top, #9dd53a 0%, #a1d54f 50%, #80c217 51%, #7cbc0a 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#9dd53a), color-stop(50%,#a1d54f), color-stop(51%,#80c217), color-stop(100%,#7cbc0a)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* IE10+ */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#9dd53a', endColorstr='#7cbc0a',GradientType=0 ); /* IE6-9 */ + background: linear-gradient(top, #9dd53a 0%,#a1d54f 50%,#80c217 51%,#7cbc0a 100%); /* W3C */ +} diff --git a/public/novnc/include/blue.css b/public/novnc/include/blue.css new file mode 100644 index 00000000..4ab53bde --- /dev/null +++ b/public/novnc/include/blue.css @@ -0,0 +1,64 @@ +/* + * noVNC blue CSS + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +.noVNC_status_normal { + background-color:#04073d; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(10,15,79)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(10,15,79) 54%, + rgb(4,7,61) 50% + ); +} +.noVNC_status_error { + background-color:#f04040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,64,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(249,64,64) 50% + ); +} +.noVNC_status_warn { + background-color:#f0f040; + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.54, rgb(240,240,64)), + color-stop(0.5, rgb(4,7,61)) + ); + background-image: -moz-linear-gradient( + center bottom, + rgb(4,7,61) 54%, + rgb(240,240,64) 50% + ); +} + +.triangle-right { + border:2px solid #fff; + background:#04073d; + color:#fff; +} + +#noVNC_keyboardinput { + background-color:#04073d; +} + diff --git a/public/novnc/include/chrome-app/tcp-client.js b/public/novnc/include/chrome-app/tcp-client.js new file mode 100644 index 00000000..b8c125f5 --- /dev/null +++ b/public/novnc/include/chrome-app/tcp-client.js @@ -0,0 +1,321 @@ +/* +Copyright 2012 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Author: Boris Smus (smus@chromium.org) +*/ + +(function(exports) { + + // Define some local variables here. + var socket = chrome.socket || chrome.experimental.socket; + var dns = chrome.experimental.dns; + + /** + * Creates an instance of the client + * + * @param {String} host The remote host to connect to + * @param {Number} port The port to connect to at the remote host + */ + function TcpClient(host, port, pollInterval) { + this.host = host; + this.port = port; + this.pollInterval = pollInterval || 15; + + // Callback functions. + this.callbacks = { + connect: null, // Called when socket is connected. + disconnect: null, // Called when socket is disconnected. + recvBuffer: null, // Called (as ArrayBuffer) when client receives data from server. + recvString: null, // Called (as string) when client receives data from server. + sent: null // Called when client sends data to server. + }; + + // Socket. + this.socketId = null; + this.isConnected = false; + + log('initialized tcp client'); + } + + /** + * Connects to the TCP socket, and creates an open socket. + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-create + * @param {Function} callback The function to call on connection + */ + TcpClient.prototype.connect = function(callback) { + // First resolve the hostname to an IP. + dns.resolve(this.host, function(result) { + this.addr = result.address; + socket.create('tcp', {}, this._onCreate.bind(this)); + + // Register connect callback. + this.callbacks.connect = callback; + }.bind(this)); + }; + + /** + * Sends an arraybuffer/view down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The arraybuffer/view to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendBuffer = function(buf, callback) { + if (buf.buffer) { + buf = buf.buffer; + } + + /* + // Debug + var bytes = [], u8 = new Uint8Array(buf); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("sending bytes: " + (bytes.join(','))); + */ + + socket.write(this.socketId, buf, this._onWriteComplete.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sends a string down the wire to the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-write + * @param {String} msg The string to send + * @param {Function} callback The function to call when the message has sent + */ + TcpClient.prototype.sendString = function(msg, callback) { + /* + // Debug + log("sending string: " + msg); + */ + + this._stringToArrayBuffer(msg, function(arrayBuffer) { + socket.write(this.socketId, arrayBuffer, this._onWriteComplete.bind(this)); + }.bind(this)); + + // Register sent callback. + this.callbacks.sent = callback; + }; + + /** + * Sets the callback for when a message is received + * + * @param {Function} callback The function to call when a message has arrived + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addResponseListener = function(callback, type) { + if (typeof type === "undefined") { + type = "arraybuffer"; + } + // Register received callback. + if (type === "string") { + this.callbacks.recvString = callback; + } else { + this.callbacks.recvBuffer = callback; + } + }; + + /** + * Sets the callback for when the socket disconnects + * + * @param {Function} callback The function to call when the socket disconnects + * @param {String} type The callback argument type: "arraybuffer" or "string" + */ + TcpClient.prototype.addDisconnectListener = function(callback) { + // Register disconnect callback. + this.callbacks.disconnect = callback; + }; + + /** + * Disconnects from the remote side + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-disconnect + */ + TcpClient.prototype.disconnect = function() { + if (this.isConnected) { + this.isConnected = false; + socket.disconnect(this.socketId); + if (this.callbacks.disconnect) { + this.callbacks.disconnect(); + } + log('socket disconnected'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * create a socket. If the socket is successfully created + * we go ahead and connect to the remote side. + * + * @private + * @see http://developer.chrome.com/trunk/apps/socket.html#method-connect + * @param {Object} createInfo The socket details + */ + TcpClient.prototype._onCreate = function(createInfo) { + this.socketId = createInfo.socketId; + if (this.socketId > 0) { + socket.connect(this.socketId, this.addr, this.port, this._onConnectComplete.bind(this)); + } else { + error('Unable to create socket'); + } + }; + + /** + * The callback function used for when we attempt to have Chrome + * connect to the remote side. If a successful connection is + * made then polling starts to check for data to read + * + * @private + * @param {Number} resultCode Indicates whether the connection was successful + */ + TcpClient.prototype._onConnectComplete = function(resultCode) { + // Start polling for reads. + this.isConnected = true; + setTimeout(this._periodicallyRead.bind(this), this.pollInterval); + + if (this.callbacks.connect) { + log('connect complete'); + this.callbacks.connect(); + } + log('onConnectComplete'); + }; + + /** + * Checks for new data to read from the socket + * + * @see http://developer.chrome.com/trunk/apps/socket.html#method-read + */ + TcpClient.prototype._periodicallyRead = function() { + var that = this; + socket.getInfo(this.socketId, function (info) { + if (info.connected) { + setTimeout(that._periodicallyRead.bind(that), that.pollInterval); + socket.read(that.socketId, null, that._onDataRead.bind(that)); + } else if (that.isConnected) { + log('socket disconnect detected'); + that.disconnect(); + } + }); + }; + + /** + * Callback function for when data has been read from the socket. + * Converts the array buffer that is read in to a string + * and sends it on for further processing by passing it to + * the previously assigned callback function. + * + * @private + * @see TcpClient.prototype.addResponseListener + * @param {Object} readInfo The incoming message + */ + TcpClient.prototype._onDataRead = function(readInfo) { + // Call received callback if there's data in the response. + if (readInfo.resultCode > 0) { + log('onDataRead'); + + /* + // Debug + var bytes = [], u8 = new Uint8Array(readInfo.data); + for (var i = 0; i < u8.length; i++) { + bytes.push(u8[i]); + } + log("received bytes: " + (bytes.join(','))); + */ + + if (this.callbacks.recvBuffer) { + // Return raw ArrayBuffer directly. + this.callbacks.recvBuffer(readInfo.data); + } + if (this.callbacks.recvString) { + // Convert ArrayBuffer to string. + this._arrayBufferToString(readInfo.data, function(str) { + this.callbacks.recvString(str); + }.bind(this)); + } + + // Trigger another read right away + setTimeout(this._periodicallyRead.bind(this), 0); + } + }; + + /** + * Callback for when data has been successfully + * written to the socket. + * + * @private + * @param {Object} writeInfo The outgoing message + */ + TcpClient.prototype._onWriteComplete = function(writeInfo) { + log('onWriteComplete'); + // Call sent callback. + if (this.callbacks.sent) { + this.callbacks.sent(writeInfo); + } + }; + + /** + * Converts an array buffer to a string + * + * @private + * @param {ArrayBuffer} buf The buffer to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._arrayBufferToString = function(buf, callback) { + var bb = new Blob([new Uint8Array(buf)]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsText(bb); + }; + + /** + * Converts a string to an array buffer + * + * @private + * @param {String} str The string to convert + * @param {Function} callback The function to call when conversion is complete + */ + TcpClient.prototype._stringToArrayBuffer = function(str, callback) { + var bb = new Blob([str]); + var f = new FileReader(); + f.onload = function(e) { + callback(e.target.result); + }; + f.readAsArrayBuffer(bb); + }; + + /** + * Wrapper function for logging + */ + function log(msg) { + console.log(msg); + } + + /** + * Wrapper function for error logging + */ + function error(msg) { + console.error(msg); + } + + exports.TcpClient = TcpClient; + +})(window); diff --git a/public/novnc/include/des.js b/public/novnc/include/des.js new file mode 100644 index 00000000..ecbc819e --- /dev/null +++ b/public/novnc/include/des.js @@ -0,0 +1,276 @@ +/* + * Ported from Flashlight VNC ActionScript implementation: + * http://www.wizhelp.com/flashlight-vnc/ + * + * Full attribution follows: + * + * ------------------------------------------------------------------------- + * + * This DES class has been extracted from package Acme.Crypto for use in VNC. + * The unnecessary odd parity code has been removed. + * + * These changes are: + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + + * DesCipher - the DES encryption method + * + * The meat of this code is by Dave Zimmerman , and is: + * + * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved. + * + * Permission to use, copy, modify, and distribute this software + * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and + * without fee is hereby granted, provided that this copyright notice is kept + * intact. + * + * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY + * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE + * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. + * + * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE + * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE + * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT + * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE + * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE + * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE + * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP + * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR + * HIGH RISK ACTIVITIES. + * + * + * The rest is: + * + * Copyright (C) 1996 by Jef Poskanzer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Visit the ACME Labs Java page for up-to-date versions of this and other + * fine Java utilities: http://www.acme.com/java/ + */ + +/* jslint white: false */ + +function DES(passwd) { + "use strict"; + + // Tables, permutations, S-boxes, etc. + // jshint -W013 + var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], + z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, + keys = []; + + // jshint -W015 + a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; + SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, + z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, + a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, + c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; + a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; + SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, + a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, + z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, + z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; + a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; + SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, + b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, + c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, + b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; + a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; + SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, + z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, + b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, + c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; + a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; + SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, + a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, + z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, + c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; + a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; + SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, + z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, + b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, + a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; + a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; + SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, + b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, + b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, + z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; + a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; + SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, + c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, + a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, + z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + // jshint +W013,+W015 + + // Set the key. + function setKeys(keyBlock) { + var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], + raw0, raw1, rawi, KnLi; + + for (j = 0, l = 56; j < 56; ++j, l -= 8) { + l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 + m = l & 0x7; + pc1m[j] = ((keyBlock[l >>> 3] & (1<>> 10; + keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + keys[KnLi] = (raw0 & 0x0003f000) << 12; + keys[KnLi] |= (raw0 & 0x0000003f) << 16; + keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } + } + + // Encrypt 8 bytes of text + function enc8(text) { + var i = 0, b = text.slice(), fval, keysi = 0, + l, r, x; // left, right, accumulator + + // Squash 8 bytes to 2 ints + l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); + + for (i = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } + + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); + + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; + } + + // Encrypt 16 bytes of text using passwd as key + function encrypt(t) { + return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16))); + } + + setKeys(passwd); // Setup keys + return {'encrypt': encrypt}; // Public interface + +} // function DES diff --git a/public/novnc/include/display.js b/public/novnc/include/display.js new file mode 100644 index 00000000..a492817d --- /dev/null +++ b/public/novnc/include/display.js @@ -0,0 +1,908 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2015 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/*jslint browser: true, white: false */ +/*global Util, Base64, changeCursor */ + +var Display; + +(function () { + "use strict"; + + var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; + try { + new ImageData(new Uint8ClampedArray(1), 1, 1); + SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; + } catch (ex) { + // ignore failure + } + + Display = function (defaults) { + this._drawCtx = null; + this._c_forceCanvas = false; + + this._renderQ = []; // queue drawing actions for in-oder rendering + + // the full frame buffer (logical canvas) size + this._fb_width = 0; + this._fb_height = 0; + + // the size limit of the viewport (start disabled) + this._maxWidth = 0; + this._maxHeight = 0; + + // the visible "physical canvas" viewport + this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 }; + this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 }; + + this._prevDrawStyle = ""; + this._tile = null; + this._tile16x16 = null; + this._tile_x = 0; + this._tile_y = 0; + + Util.set_defaults(this, defaults, { + 'true_color': true, + 'colourMap': [], + 'scale': 1.0, + 'viewport': false, + 'render_mode': '' + }); + + Util.Debug(">> Display.constructor"); + + if (!this._target) { + throw new Error("Target must be set"); + } + + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } + + if (!this._target.getContext) { + throw new Error("no getContext method"); + } + + if (!this._drawCtx) { + this._drawCtx = this._target.getContext('2d'); + } + + Util.Debug("User Agent: " + navigator.userAgent); + if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); } + if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); } + if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); } + if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); } + + this.clear(); + + // Check canvas features + if ('createImageData' in this._drawCtx) { + this._render_mode = 'canvas rendering'; + } else { + throw new Error("Canvas does not support createImageData"); + } + + if (this._prefer_js === null) { + Util.Info("Prefering javascript operations"); + this._prefer_js = true; + } + + // Determine browser support for setting the cursor via data URI scheme + if (this._cursor_uri || this._cursor_uri === null || + this._cursor_uri === undefined) { + this._cursor_uri = Util.browserSupportsCursorURIs(); + } + + Util.Debug("<< Display.constructor"); + }; + + Display.prototype = { + // Public methods + viewportChangePos: function (deltaX, deltaY) { + var vp = this._viewportLoc; + deltaX = Math.floor(deltaX); + deltaY = Math.floor(deltaY); + + if (!this._viewport) { + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; + } + + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + + // Position change + + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; + } + + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); + } + + if (deltaX === 0 && deltaY === 0) { + return; + } + Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); + + vp.x += deltaX; + vx2 += deltaX; + vp.y += deltaY; + vy2 += deltaY; + + // Update the clean rectangle + var cr = this._cleanRect; + if (vp.x > cr.x1) { + cr.x1 = vp.x; + } + if (vx2 < cr.x2) { + cr.x2 = vx2; + } + if (vp.y > cr.y1) { + cr.y1 = vp.y; + } + if (vy2 < cr.y2) { + cr.y2 = vy2; + } + + var x1, w; + if (deltaX < 0) { + // Shift viewport left, redraw left section + x1 = 0; + w = -deltaX; + } else { + // Shift viewport right, redraw right section + x1 = vp.w - deltaX; + w = deltaX; + } + + var y1, h; + if (deltaY < 0) { + // Shift viewport up, redraw top section + y1 = 0; + h = -deltaY; + } else { + // Shift viewport down, redraw bottom section + y1 = vp.h - deltaY; + h = deltaY; + } + + var saveStyle = this._drawCtx.fillStyle; + var canvas = this._target; + this._drawCtx.fillStyle = "rgb(255,255,255)"; + + // Due to this bug among others [1] we need to disable the image-smoothing to + // avoid getting a blur effect when panning. + // + // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 + // + // We need to set these every time since all properties are reset + // when the the size is changed + if (this._drawCtx.mozImageSmoothingEnabled) { + this._drawCtx.mozImageSmoothingEnabled = false; + } else if (this._drawCtx.webkitImageSmoothingEnabled) { + this._drawCtx.webkitImageSmoothingEnabled = false; + } else if (this._drawCtx.msImageSmoothingEnabled) { + this._drawCtx.msImageSmoothingEnabled = false; + } else if (this._drawCtx.imageSmoothingEnabled) { + this._drawCtx.imageSmoothingEnabled = false; + } + + // Copy the valid part of the viewport to the shifted location + this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, -deltaY, vp.w, vp.h); + + if (deltaX !== 0) { + this._drawCtx.fillRect(x1, 0, w, vp.h); + } + if (deltaY !== 0) { + this._drawCtx.fillRect(0, y1, vp.w, h); + } + this._drawCtx.fillStyle = saveStyle; + }, + + viewportChangeSize: function(width, height) { + + if (typeof(width) === "undefined" || typeof(height) === "undefined") { + + Util.Debug("Setting viewport to full display region"); + width = this._fb_width; + height = this._fb_height; + } + + var vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + + if (this._viewport) { + if (this._maxWidth !== 0 && width > this._maxWidth) { + width = this._maxWidth; + } + if (this._maxHeight !== 0 && height > this._maxHeight) { + height = this._maxHeight; + } + } + + var cr = this._cleanRect; + + if (width < vp.w && cr.x2 > vp.x + width - 1) { + cr.x2 = vp.x + width - 1; + } + if (height < vp.h && cr.y2 > vp.y + height - 1) { + cr.y2 = vp.y + height - 1; + } + + vp.w = width; + vp.h = height; + + var canvas = this._target; + if (canvas.width !== width || canvas.height !== height) { + + // We have to save the canvas data since changing the size will clear it + var saveImg = null; + if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { + var img_width = canvas.width < vp.w ? canvas.width : vp.w; + var img_height = canvas.height < vp.h ? canvas.height : vp.h; + saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); + } + + if (canvas.width !== width) { + canvas.width = width; + canvas.style.width = width + 'px'; + } + if (canvas.height !== height) { + canvas.height = height; + canvas.style.height = height + 'px'; + } + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } + } + } + }, + + // Return a map of clean and dirty areas of the viewport and reset the + // tracking of clean and dirty areas + // + // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h}, + // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] } + getCleanDirtyReset: function () { + var vp = this._viewportLoc; + var cr = this._cleanRect; + + var cleanBox = { 'x': cr.x1, 'y': cr.y1, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 }; + + var dirtyBoxes = []; + if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) { + // Whole viewport is dirty + dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h }); + } else { + // Redraw dirty regions + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + + if (vp.x < cr.x1) { + // left side dirty region + dirtyBoxes.push({'x': vp.x, 'y': vp.y, + 'w': cr.x1 - vp.x + 1, 'h': vp.h}); + } + if (vx2 > cr.x2) { + // right side dirty region + dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y, + 'w': vx2 - cr.x2, 'h': vp.h}); + } + if(vp.y < cr.y1) { + // top/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': vp.y, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y}); + } + if (vy2 > cr.y2) { + // bottom/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1, + 'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2}); + } + } + + this._cleanRect = {'x1': vp.x, 'y1': vp.y, + 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1}; + + return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; + }, + + absX: function (x) { + return x + this._viewportLoc.x; + }, + + absY: function (y) { + return y + this._viewportLoc.y; + }, + + resize: function (width, height) { + this._prevDrawStyle = ""; + + this._fb_width = width; + this._fb_height = height; + + this._rescale(this._scale); + + this.viewportChangeSize(); + }, + + clear: function () { + if (this._logo) { + this.resize(this._logo.width, this._logo.height); + this.blitStringImage(this._logo.data, 0, 0); + } else { + if (Util.Engine.trident === 6) { + // NB(directxman12): there's a bug in IE10 where we can fail to actually + // clear the canvas here because of the resize. + // Clearing the current viewport first fixes the issue + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } + this.resize(240, 20); + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } + + this._renderQ = []; + }, + + fillRect: function (x, y, width, height, color, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'fill', + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'color': color + }); + } else { + this._setFillColor(color); + this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height); + } + }, + + copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'copy', + 'old_x': old_x, + 'old_y': old_y, + 'x': new_x, + 'y': new_y, + 'width': w, + 'height': h, + }); + } else { + var x1 = old_x - this._viewportLoc.x; + var y1 = old_y - this._viewportLoc.y; + var x2 = new_x - this._viewportLoc.x; + var y2 = new_y - this._viewportLoc.y; + + this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h); + } + }, + + // start updating a tile + startTile: function (x, y, width, height, color) { + this._tile_x = x; + this._tile_y = y; + if (width === 16 && height === 16) { + this._tile = this._tile16x16; + } else { + this._tile = this._drawCtx.createImageData(width, height); + } + + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + + var data = this._tile.data; + for (var i = 0; i < width * height * 4; i += 4) { + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = 255; + } + } else { + this.fillRect(x, y, width, height, color, true); + } + }, + + // update sub-rectangle of the current tile + subTile: function (x, y, w, h, color) { + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + var xend = x + w; + var yend = y + h; + + var data = this._tile.data; + var width = this._tile.width; + for (var j = y; j < yend; j++) { + for (var i = x; i < xend; i++) { + var p = (i + (j * width)) * 4; + data[p] = red; + data[p + 1] = green; + data[p + 2] = blue; + data[p + 3] = 255; + } + } + } else { + this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true); + } + }, + + // draw the current tile to the screen + finishTile: function () { + if (this._prefer_js) { + this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x, + this._tile_y - this._viewportLoc.y); + } + // else: No-op -- already done by setSubTile + }, + + blitImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this.renderQ_push({ + 'type': 'blit', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { + this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, + + blitRgbImage: function (x, y , width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this.renderQ_push({ + 'type': 'blitRgb', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { + this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } else { + // probably wrong? + this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, + + blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this.renderQ_push({ + 'type': 'blitRgbx', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } + }, + + blitStringImage: function (str, x, y) { + var img = new Image(); + img.onload = function () { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }.bind(this); + img.src = str; + return img; // for debugging purposes + }, + + // wrap ctx.drawImage but relative to viewport + drawImage: function (img, x, y) { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }, + + renderQ_push: function (action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will start polling the queue (every + // requestAnimationFrame interval) + this._scan_renderQ(); + } + }, + + changeCursor: function (pixels, mask, hotx, hoty, w, h) { + if (this._cursor_uri === false) { + Util.Warn("changeCursor called but no cursor data URI support"); + return; + } + + if (this._true_color) { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); + } else { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); + } + }, + + defaultCursor: function () { + this._target.style.cursor = "default"; + }, + + disableLocalCursor: function () { + this._target.style.cursor = "none"; + }, + + clippingDisplay: function () { + var vp = this._viewportLoc; + + var fbClip = this._fb_width > vp.w || this._fb_height > vp.h; + var limitedVp = this._maxWidth !== 0 && this._maxHeight !== 0; + var clipping = false; + + if (limitedVp) { + clipping = vp.w > this._maxWidth || vp.h > this._maxHeight; + } + + return fbClip || (limitedVp && clipping); + }, + + // Overridden getters/setters + get_context: function () { + return this._drawCtx; + }, + + set_scale: function (scale) { + this._rescale(scale); + }, + + set_width: function (w) { + this._fb_width = w; + }, + get_width: function () { + return this._fb_width; + }, + + set_height: function (h) { + this._fb_height = h; + }, + get_height: function () { + return this._fb_height; + }, + + autoscale: function (containerWidth, containerHeight, downscaleOnly) { + var targetAspectRatio = containerWidth / containerHeight; + var fbAspectRatio = this._fb_width / this._fb_height; + + var scaleRatio; + if (fbAspectRatio >= targetAspectRatio) { + scaleRatio = containerWidth / this._fb_width; + } else { + scaleRatio = containerHeight / this._fb_height; + } + + var targetW, targetH; + if (scaleRatio > 1.0 && downscaleOnly) { + targetW = this._fb_width; + targetH = this._fb_height; + scaleRatio = 1.0; + } else if (fbAspectRatio >= targetAspectRatio) { + targetW = containerWidth; + targetH = Math.round(containerWidth / fbAspectRatio); + } else { + targetW = Math.round(containerHeight * fbAspectRatio); + targetH = containerHeight; + } + + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. + this._target.style.width = targetW + 'px'; + this._target.style.height = targetH + 'px'; + + this._scale = scaleRatio; + + return scaleRatio; // so that the mouse, etc scale can be set + }, + + // Private Methods + _rescale: function (factor) { + this._scale = factor; + + var w; + var h; + + if (this._viewport && + this._maxWidth !== 0 && this._maxHeight !== 0) { + w = Math.min(this._fb_width, this._maxWidth); + h = Math.min(this._fb_height, this._maxHeight); + } else { + w = this._fb_width; + h = this._fb_height; + } + + this._target.style.width = Math.round(factor * w) + 'px'; + this._target.style.height = Math.round(factor * h) + 'px'; + }, + + _setFillColor: function (color) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color]; + } + + var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; + if (newStyle !== this._prevDrawStyle) { + this._drawCtx.fillStyle = newStyle; + this._prevDrawStyle = newStyle; + } + }, + + _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { + data[i] = arr[j]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { + data[i] = arr[j + 2]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _rgbxImageData: function (x, y, vx, vy, width, height, arr, offset) { + // NB(directxman12): arr must be an Type Array view + var img; + if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { + img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); + } else { + img = this._drawCtx.createImageData(width, height); + img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) { + var img = this._drawCtx.createImageData(width, height); + var data = img.data; + var cmap = this._colourMap; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { + var bgr = cmap[arr[j]]; + data[i] = bgr[2]; + data[i + 1] = bgr[1]; + data[i + 2] = bgr[0]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _scan_renderQ: function () { + var ready = true; + while (ready && this._renderQ.length > 0) { + var a = this._renderQ[0]; + switch (a.type) { + case 'copy': + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color, true); + break; + case 'blit': + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgb': + this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgbx': + this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'img': + if (a.img.complete) { + this.drawImage(a.img, a.x, a.y); + } else { + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + + if (ready) { + this._renderQ.shift(); + } + } + + if (this._renderQ.length > 0) { + requestAnimFrame(this._scan_renderQ.bind(this)); + } + }, + }; + + Util.make_properties(Display, [ + ['target', 'wo', 'dom'], // Canvas element for rendering + ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only) + ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data} + ['true_color', 'rw', 'bool'], // Use true-color pixel data + ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) + ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0 + ['viewport', 'rw', 'bool'], // Use viewport clipping + ['width', 'rw', 'int'], // Display area width + ['height', 'rw', 'int'], // Display area height + ['maxWidth', 'rw', 'int'], // Viewport max width (0 if disabled) + ['maxHeight', 'rw', 'int'], // Viewport max height (0 if disabled) + + ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) + + ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods + ['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI + ]); + + // Class Methods + Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) { + var w = w0; + var h = h0; + if (h < w) { + h = w; // increase h to make it square + } else { + w = h; // increase w to make it square + } + + var cur = []; + + // Push multi-byte little-endian values + cur.push16le = function (num) { + this.push(num & 0xFF, (num >> 8) & 0xFF); + }; + cur.push32le = function (num) { + this.push(num & 0xFF, + (num >> 8) & 0xFF, + (num >> 16) & 0xFF, + (num >> 24) & 0xFF); + }; + + var IHDRsz = 40; + var RGBsz = w * h * 4; + var XORsz = Math.ceil((w * h) / 8.0); + var ANDsz = Math.ceil((w * h) / 8.0); + + cur.push16le(0); // 0: Reserved + cur.push16le(2); // 2: .CUR type + cur.push16le(1); // 4: Number of images, 1 for non-animated ico + + // Cursor #1 header (ICONDIRENTRY) + cur.push(w); // 6: width + cur.push(h); // 7: height + cur.push(0); // 8: colors, 0 -> true-color + cur.push(0); // 9: reserved + cur.push16le(hotx); // 10: hotspot x coordinate + cur.push16le(hoty); // 12: hotspot y coordinate + cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); + // 14: cursor data byte size + cur.push32le(22); // 18: offset of cursor data in the file + + // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) + cur.push32le(IHDRsz); // 22: InfoHeader size + cur.push32le(w); // 26: Cursor width + cur.push32le(h * 2); // 30: XOR+AND height + cur.push16le(1); // 34: number of planes + cur.push16le(32); // 36: bits per pixel + cur.push32le(0); // 38: Type of compression + + cur.push32le(XORsz + ANDsz); + // 42: Size of Image + cur.push32le(0); // 46: reserved + cur.push32le(0); // 50: reserved + cur.push32le(0); // 54: reserved + cur.push32le(0); // 58: reserved + + // 62: color data (RGBQUAD icColors[]) + var y, x; + for (y = h - 1; y >= 0; y--) { + for (x = 0; x < w; x++) { + if (x >= w0 || y >= h0) { + cur.push(0); // blue + cur.push(0); // green + cur.push(0); // red + cur.push(0); // alpha + } else { + var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); + var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + if (cmap) { + idx = (w0 * y) + x; + var rgb = cmap[pixels[idx]]; + cur.push(rgb[2]); // blue + cur.push(rgb[1]); // green + cur.push(rgb[0]); // red + cur.push(alpha); // alpha + } else { + idx = ((w0 * y) + x) * 4; + cur.push(pixels[idx + 2]); // blue + cur.push(pixels[idx + 1]); // green + cur.push(pixels[idx]); // red + cur.push(alpha); // alpha + } + } + } + } + + // XOR/bitmask data (BYTE icXOR[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } + } + + // AND/bitmask data (BYTE icAND[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } + } + + var url = 'data:image/x-icon;base64,' + Base64.encode(cur); + target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; + }; +})(); diff --git a/public/novnc/include/inflator.js b/public/novnc/include/inflator.js new file mode 100644 index 00000000..48ede208 --- /dev/null +++ b/public/novnc/include/inflator.js @@ -0,0 +1,2418 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.inflator = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o>> 16) & 0xffff) |0, + n = 0; + + while (len !== 0) { + // Set limit ~ twice less than 5552, to keep + // s2 in 31-bits, because we force signed ints. + // in other case %= will fail. + n = len > 2000 ? 2000 : len; + len -= n; + + do { + s1 = (s1 + buf[pos++]) |0; + s2 = (s2 + s1) |0; + } while (--n); + + s1 %= 65521; + s2 %= 65521; + } + + return (s1 | (s2 << 16)) |0; +} + + +module.exports = adler32; + +},{}],3:[function(require,module,exports){ +'use strict'; + +// Note: we can't get significant speed boost here. +// So write code to minimize size - no pregenerated tables +// and array tools dependencies. + + +// Use ordinary array, since untyped makes no boost here +function makeTable() { + var c, table = []; + + for (var n =0; n < 256; n++) { + c = n; + for (var k =0; k < 8; k++) { + c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); + } + table[n] = c; + } + + return table; +} + +// Create table on load. Just 255 signed longs. Not a problem. +var crcTable = makeTable(); + + +function crc32(crc, buf, len, pos) { + var t = crcTable, + end = pos + len; + + crc = crc ^ (-1); + + for (var i = pos; i < end; i++) { + crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF]; + } + + return (crc ^ (-1)); // >>> 0; +} + + +module.exports = crc32; + +},{}],4:[function(require,module,exports){ +'use strict'; + +// See state defs from inflate.js +var BAD = 30; /* got a data error -- remain here until reset */ +var TYPE = 12; /* i: waiting for type bits, including last-flag bit */ + +/* + Decode literal, length, and distance codes and write out the resulting + literal and match bytes until either not enough input or output is + available, an end-of-block is encountered, or a data error is encountered. + When large enough input and output buffers are supplied to inflate(), for + example, a 16K input buffer and a 64K output buffer, more than 95% of the + inflate execution time is spent in this routine. + + Entry assumptions: + + state.mode === LEN + strm.avail_in >= 6 + strm.avail_out >= 258 + start >= strm.avail_out + state.bits < 8 + + On return, state.mode is one of: + + LEN -- ran out of enough output space or enough available input + TYPE -- reached end of block code, inflate() to interpret next block + BAD -- error in block data + + Notes: + + - The maximum input bits used by a length/distance pair is 15 bits for the + length code, 5 bits for the length extra, 15 bits for the distance code, + and 13 bits for the distance extra. This totals 48 bits, or six bytes. + Therefore if strm.avail_in >= 6, then there is enough input to avoid + checking for available input while decoding. + + - The maximum bytes that a single length/distance pair can output is 258 + bytes, which is the maximum length that can be coded. inflate_fast() + requires strm.avail_out >= 258 for each loop to avoid checking for + output space. + */ +module.exports = function inflate_fast(strm, start) { + var state; + var _in; /* local strm.input */ + var last; /* have enough input while in < last */ + var _out; /* local strm.output */ + var beg; /* inflate()'s initial strm.output */ + var end; /* while out < end, enough space available */ +//#ifdef INFLATE_STRICT + var dmax; /* maximum distance from zlib header */ +//#endif + var wsize; /* window size or zero if not using window */ + var whave; /* valid bytes in the window */ + var wnext; /* window write index */ + // Use `s_window` instead `window`, avoid conflict with instrumentation tools + var s_window; /* allocated sliding window, if wsize != 0 */ + var hold; /* local strm.hold */ + var bits; /* local strm.bits */ + var lcode; /* local strm.lencode */ + var dcode; /* local strm.distcode */ + var lmask; /* mask for first level of length codes */ + var dmask; /* mask for first level of distance codes */ + var here; /* retrieved table entry */ + var op; /* code bits, operation, extra bits, or */ + /* window position, window bytes to copy */ + var len; /* match length, unused bytes */ + var dist; /* match distance */ + var from; /* where to copy match from */ + var from_source; + + + var input, output; // JS specific, because we have no pointers + + /* copy state to local variables */ + state = strm.state; + //here = state.here; + _in = strm.next_in; + input = strm.input; + last = _in + (strm.avail_in - 5); + _out = strm.next_out; + output = strm.output; + beg = _out - (start - strm.avail_out); + end = _out + (strm.avail_out - 257); +//#ifdef INFLATE_STRICT + dmax = state.dmax; +//#endif + wsize = state.wsize; + whave = state.whave; + wnext = state.wnext; + s_window = state.window; + hold = state.hold; + bits = state.bits; + lcode = state.lencode; + dcode = state.distcode; + lmask = (1 << state.lenbits) - 1; + dmask = (1 << state.distbits) - 1; + + + /* decode literals and length/distances until end-of-block or not enough + input data or output space */ + + top: + do { + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + + here = lcode[hold & lmask]; + + dolen: + for (;;) { // Goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = (here >>> 16) & 0xff/*here.op*/; + if (op === 0) { /* literal */ + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + output[_out++] = here & 0xffff/*here.val*/; + } + else if (op & 16) { /* length base */ + len = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (op) { + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + len += hold & ((1 << op) - 1); + hold >>>= op; + bits -= op; + } + //Tracevv((stderr, "inflate: length %u\n", len)); + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + here = dcode[hold & dmask]; + + dodist: + for (;;) { // goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = (here >>> 16) & 0xff/*here.op*/; + + if (op & 16) { /* distance base */ + dist = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + } + dist += hold & ((1 << op) - 1); +//#ifdef INFLATE_STRICT + if (dist > dmax) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break top; + } +//#endif + hold >>>= op; + bits -= op; + //Tracevv((stderr, "inflate: distance %u\n", dist)); + op = _out - beg; /* max distance in output */ + if (dist > op) { /* see if copy from window */ + op = dist - op; /* distance back in window */ + if (op > whave) { + if (state.sane) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break top; + } + +// (!) This block is disabled in zlib defailts, +// don't enable it for binary compatibility +//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR +// if (len <= op - whave) { +// do { +// output[_out++] = 0; +// } while (--len); +// continue top; +// } +// len -= op - whave; +// do { +// output[_out++] = 0; +// } while (--op > whave); +// if (op === 0) { +// from = _out - dist; +// do { +// output[_out++] = output[from++]; +// } while (--len); +// continue top; +// } +//#endif + } + from = 0; // window index + from_source = s_window; + if (wnext === 0) { /* very common case */ + from += wsize - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + else if (wnext < op) { /* wrap around window */ + from += wsize + wnext - op; + op -= wnext; + if (op < len) { /* some from end of window */ + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = 0; + if (wnext < len) { /* some from start of window */ + op = wnext; + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + } + else { /* contiguous in window */ + from += wnext - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = s_window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + while (len > 2) { + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + len -= 3; + } + if (len) { + output[_out++] = from_source[from++]; + if (len > 1) { + output[_out++] = from_source[from++]; + } + } + } + else { + from = _out - dist; /* copy direct from output */ + do { /* minimum length is three */ + output[_out++] = output[from++]; + output[_out++] = output[from++]; + output[_out++] = output[from++]; + len -= 3; + } while (len > 2); + if (len) { + output[_out++] = output[from++]; + if (len > 1) { + output[_out++] = output[from++]; + } + } + } + } + else if ((op & 64) === 0) { /* 2nd level distance code */ + here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; + continue dodist; + } + else { + strm.msg = 'invalid distance code'; + state.mode = BAD; + break top; + } + + break; // need to emulate goto via "continue" + } + } + else if ((op & 64) === 0) { /* 2nd level length code */ + here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; + continue dolen; + } + else if (op & 32) { /* end-of-block */ + //Tracevv((stderr, "inflate: end of block\n")); + state.mode = TYPE; + break top; + } + else { + strm.msg = 'invalid literal/length code'; + state.mode = BAD; + break top; + } + + break; // need to emulate goto via "continue" + } + } while (_in < last && _out < end); + + /* return unused bytes (on entry, bits < 8, so in won't go too far back) */ + len = bits >> 3; + _in -= len; + bits -= len << 3; + hold &= (1 << bits) - 1; + + /* update state and return */ + strm.next_in = _in; + strm.next_out = _out; + strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last)); + strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end)); + state.hold = hold; + state.bits = bits; + return; +}; + +},{}],5:[function(require,module,exports){ +'use strict'; + + +var utils = require('../utils/common'); +var adler32 = require('./adler32'); +var crc32 = require('./crc32'); +var inflate_fast = require('./inffast'); +var inflate_table = require('./inftrees'); + +var CODES = 0; +var LENS = 1; +var DISTS = 2; + +/* Public constants ==========================================================*/ +/* ===========================================================================*/ + + +/* Allowed flush values; see deflate() and inflate() below for details */ +//var Z_NO_FLUSH = 0; +//var Z_PARTIAL_FLUSH = 1; +//var Z_SYNC_FLUSH = 2; +//var Z_FULL_FLUSH = 3; +var Z_FINISH = 4; +var Z_BLOCK = 5; +var Z_TREES = 6; + + +/* Return codes for the compression/decompression functions. Negative values + * are errors, positive values are used for special but normal events. + */ +var Z_OK = 0; +var Z_STREAM_END = 1; +var Z_NEED_DICT = 2; +//var Z_ERRNO = -1; +var Z_STREAM_ERROR = -2; +var Z_DATA_ERROR = -3; +var Z_MEM_ERROR = -4; +var Z_BUF_ERROR = -5; +//var Z_VERSION_ERROR = -6; + +/* The deflate compression method */ +var Z_DEFLATED = 8; + + +/* STATES ====================================================================*/ +/* ===========================================================================*/ + + +var HEAD = 1; /* i: waiting for magic header */ +var FLAGS = 2; /* i: waiting for method and flags (gzip) */ +var TIME = 3; /* i: waiting for modification time (gzip) */ +var OS = 4; /* i: waiting for extra flags and operating system (gzip) */ +var EXLEN = 5; /* i: waiting for extra length (gzip) */ +var EXTRA = 6; /* i: waiting for extra bytes (gzip) */ +var NAME = 7; /* i: waiting for end of file name (gzip) */ +var COMMENT = 8; /* i: waiting for end of comment (gzip) */ +var HCRC = 9; /* i: waiting for header crc (gzip) */ +var DICTID = 10; /* i: waiting for dictionary check value */ +var DICT = 11; /* waiting for inflateSetDictionary() call */ +var TYPE = 12; /* i: waiting for type bits, including last-flag bit */ +var TYPEDO = 13; /* i: same, but skip check to exit inflate on new block */ +var STORED = 14; /* i: waiting for stored size (length and complement) */ +var COPY_ = 15; /* i/o: same as COPY below, but only first time in */ +var COPY = 16; /* i/o: waiting for input or output to copy stored block */ +var TABLE = 17; /* i: waiting for dynamic block table lengths */ +var LENLENS = 18; /* i: waiting for code length code lengths */ +var CODELENS = 19; /* i: waiting for length/lit and distance code lengths */ +var LEN_ = 20; /* i: same as LEN below, but only first time in */ +var LEN = 21; /* i: waiting for length/lit/eob code */ +var LENEXT = 22; /* i: waiting for length extra bits */ +var DIST = 23; /* i: waiting for distance code */ +var DISTEXT = 24; /* i: waiting for distance extra bits */ +var MATCH = 25; /* o: waiting for output space to copy string */ +var LIT = 26; /* o: waiting for output space to write literal */ +var CHECK = 27; /* i: waiting for 32-bit check value */ +var LENGTH = 28; /* i: waiting for 32-bit length (gzip) */ +var DONE = 29; /* finished check, done -- remain here until reset */ +var BAD = 30; /* got a data error -- remain here until reset */ +var MEM = 31; /* got an inflate() memory error -- remain here until reset */ +var SYNC = 32; /* looking for synchronization bytes to restart inflate() */ + +/* ===========================================================================*/ + + + +var ENOUGH_LENS = 852; +var ENOUGH_DISTS = 592; +//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS); + +var MAX_WBITS = 15; +/* 32K LZ77 window */ +var DEF_WBITS = MAX_WBITS; + + +function ZSWAP32(q) { + return (((q >>> 24) & 0xff) + + ((q >>> 8) & 0xff00) + + ((q & 0xff00) << 8) + + ((q & 0xff) << 24)); +} + + +function InflateState() { + this.mode = 0; /* current inflate mode */ + this.last = false; /* true if processing last block */ + this.wrap = 0; /* bit 0 true for zlib, bit 1 true for gzip */ + this.havedict = false; /* true if dictionary provided */ + this.flags = 0; /* gzip header method and flags (0 if zlib) */ + this.dmax = 0; /* zlib header max distance (INFLATE_STRICT) */ + this.check = 0; /* protected copy of check value */ + this.total = 0; /* protected copy of output count */ + // TODO: may be {} + this.head = null; /* where to save gzip header information */ + + /* sliding window */ + this.wbits = 0; /* log base 2 of requested window size */ + this.wsize = 0; /* window size or zero if not using window */ + this.whave = 0; /* valid bytes in the window */ + this.wnext = 0; /* window write index */ + this.window = null; /* allocated sliding window, if needed */ + + /* bit accumulator */ + this.hold = 0; /* input bit accumulator */ + this.bits = 0; /* number of bits in "in" */ + + /* for string and stored block copying */ + this.length = 0; /* literal or length of data to copy */ + this.offset = 0; /* distance back to copy string from */ + + /* for table and code decoding */ + this.extra = 0; /* extra bits needed */ + + /* fixed and dynamic code tables */ + this.lencode = null; /* starting table for length/literal codes */ + this.distcode = null; /* starting table for distance codes */ + this.lenbits = 0; /* index bits for lencode */ + this.distbits = 0; /* index bits for distcode */ + + /* dynamic table building */ + this.ncode = 0; /* number of code length code lengths */ + this.nlen = 0; /* number of length code lengths */ + this.ndist = 0; /* number of distance code lengths */ + this.have = 0; /* number of code lengths in lens[] */ + this.next = null; /* next available space in codes[] */ + + this.lens = new utils.Buf16(320); /* temporary storage for code lengths */ + this.work = new utils.Buf16(288); /* work area for code table building */ + + /* + because we don't have pointers in js, we use lencode and distcode directly + as buffers so we don't need codes + */ + //this.codes = new utils.Buf32(ENOUGH); /* space for code tables */ + this.lendyn = null; /* dynamic table for length/literal codes (JS specific) */ + this.distdyn = null; /* dynamic table for distance codes (JS specific) */ + this.sane = 0; /* if false, allow invalid distance too far */ + this.back = 0; /* bits back of last unprocessed length/lit */ + this.was = 0; /* initial length of match */ +} + +function inflateResetKeep(strm) { + var state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + strm.total_in = strm.total_out = state.total = 0; + strm.msg = ''; /*Z_NULL*/ + if (state.wrap) { /* to support ill-conceived Java test suite */ + strm.adler = state.wrap & 1; + } + state.mode = HEAD; + state.last = 0; + state.havedict = 0; + state.dmax = 32768; + state.head = null/*Z_NULL*/; + state.hold = 0; + state.bits = 0; + //state.lencode = state.distcode = state.next = state.codes; + state.lencode = state.lendyn = new utils.Buf32(ENOUGH_LENS); + state.distcode = state.distdyn = new utils.Buf32(ENOUGH_DISTS); + + state.sane = 1; + state.back = -1; + //Tracev((stderr, "inflate: reset\n")); + return Z_OK; +} + +function inflateReset(strm) { + var state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + state.wsize = 0; + state.whave = 0; + state.wnext = 0; + return inflateResetKeep(strm); + +} + +function inflateReset2(strm, windowBits) { + var wrap; + var state; + + /* get the state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + + /* extract wrap request from windowBits parameter */ + if (windowBits < 0) { + wrap = 0; + windowBits = -windowBits; + } + else { + wrap = (windowBits >> 4) + 1; + if (windowBits < 48) { + windowBits &= 15; + } + } + + /* set number of window bits, free window if different */ + if (windowBits && (windowBits < 8 || windowBits > 15)) { + return Z_STREAM_ERROR; + } + if (state.window !== null && state.wbits !== windowBits) { + state.window = null; + } + + /* update state and reset the rest of it */ + state.wrap = wrap; + state.wbits = windowBits; + return inflateReset(strm); +} + +function inflateInit2(strm, windowBits) { + var ret; + var state; + + if (!strm) { return Z_STREAM_ERROR; } + //strm.msg = Z_NULL; /* in case we return an error */ + + state = new InflateState(); + + //if (state === Z_NULL) return Z_MEM_ERROR; + //Tracev((stderr, "inflate: allocated\n")); + strm.state = state; + state.window = null/*Z_NULL*/; + ret = inflateReset2(strm, windowBits); + if (ret !== Z_OK) { + strm.state = null/*Z_NULL*/; + } + return ret; +} + +function inflateInit(strm) { + return inflateInit2(strm, DEF_WBITS); +} + + +/* + Return state with length and distance decoding tables and index sizes set to + fixed code decoding. Normally this returns fixed tables from inffixed.h. + If BUILDFIXED is defined, then instead this routine builds the tables the + first time it's called, and returns those tables the first time and + thereafter. This reduces the size of the code by about 2K bytes, in + exchange for a little execution time. However, BUILDFIXED should not be + used for threaded applications, since the rewriting of the tables and virgin + may not be thread-safe. + */ +var virgin = true; + +var lenfix, distfix; // We have no pointers in JS, so keep tables separate + +function fixedtables(state) { + /* build fixed huffman tables if first call (may not be thread safe) */ + if (virgin) { + var sym; + + lenfix = new utils.Buf32(512); + distfix = new utils.Buf32(32); + + /* literal/length table */ + sym = 0; + while (sym < 144) { state.lens[sym++] = 8; } + while (sym < 256) { state.lens[sym++] = 9; } + while (sym < 280) { state.lens[sym++] = 7; } + while (sym < 288) { state.lens[sym++] = 8; } + + inflate_table(LENS, state.lens, 0, 288, lenfix, 0, state.work, {bits: 9}); + + /* distance table */ + sym = 0; + while (sym < 32) { state.lens[sym++] = 5; } + + inflate_table(DISTS, state.lens, 0, 32, distfix, 0, state.work, {bits: 5}); + + /* do this just once */ + virgin = false; + } + + state.lencode = lenfix; + state.lenbits = 9; + state.distcode = distfix; + state.distbits = 5; +} + + +/* + Update the window with the last wsize (normally 32K) bytes written before + returning. If window does not exist yet, create it. This is only called + when a window is already in use, or when output has been written during this + inflate call, but the end of the deflate stream has not been reached yet. + It is also called to create a window for dictionary data when a dictionary + is loaded. + + Providing output buffers larger than 32K to inflate() should provide a speed + advantage, since only the last 32K of output is copied to the sliding window + upon return from inflate(), and since all distances after the first 32K of + output will fall in the output data, making match copies simpler and faster. + The advantage may be dependent on the size of the processor's data caches. + */ +function updatewindow(strm, src, end, copy) { + var dist; + var state = strm.state; + + /* if it hasn't been done already, allocate space for the window */ + if (state.window === null) { + state.wsize = 1 << state.wbits; + state.wnext = 0; + state.whave = 0; + + state.window = new utils.Buf8(state.wsize); + } + + /* copy state->wsize or less output bytes into the circular window */ + if (copy >= state.wsize) { + utils.arraySet(state.window,src, end - state.wsize, state.wsize, 0); + state.wnext = 0; + state.whave = state.wsize; + } + else { + dist = state.wsize - state.wnext; + if (dist > copy) { + dist = copy; + } + //zmemcpy(state->window + state->wnext, end - copy, dist); + utils.arraySet(state.window,src, end - copy, dist, state.wnext); + copy -= dist; + if (copy) { + //zmemcpy(state->window, end - copy, copy); + utils.arraySet(state.window,src, end - copy, copy, 0); + state.wnext = copy; + state.whave = state.wsize; + } + else { + state.wnext += dist; + if (state.wnext === state.wsize) { state.wnext = 0; } + if (state.whave < state.wsize) { state.whave += dist; } + } + } + return 0; +} + +function inflate(strm, flush) { + var state; + var input, output; // input/output buffers + var next; /* next input INDEX */ + var put; /* next output INDEX */ + var have, left; /* available input and output */ + var hold; /* bit buffer */ + var bits; /* bits in bit buffer */ + var _in, _out; /* save starting available input and output */ + var copy; /* number of stored or match bytes to copy */ + var from; /* where to copy match bytes from */ + var from_source; + var here = 0; /* current decoding table entry */ + var here_bits, here_op, here_val; // paked "here" denormalized (JS specific) + //var last; /* parent table entry */ + var last_bits, last_op, last_val; // paked "last" denormalized (JS specific) + var len; /* length to copy for repeats, bits to drop */ + var ret; /* return code */ + var hbuf = new utils.Buf8(4); /* buffer for gzip header crc calculation */ + var opts; + + var n; // temporary var for NEED_BITS + + var order = /* permutation of code lengths */ + [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + + + if (!strm || !strm.state || !strm.output || + (!strm.input && strm.avail_in !== 0)) { + return Z_STREAM_ERROR; + } + + state = strm.state; + if (state.mode === TYPE) { state.mode = TYPEDO; } /* skip check */ + + + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + _in = have; + _out = left; + ret = Z_OK; + + inf_leave: // goto emulation + for (;;) { + switch (state.mode) { + case HEAD: + if (state.wrap === 0) { + state.mode = TYPEDO; + break; + } + //=== NEEDBITS(16); + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((state.wrap & 2) && hold === 0x8b1f) { /* gzip header */ + state.check = 0/*crc32(0L, Z_NULL, 0)*/; + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = FLAGS; + break; + } + state.flags = 0; /* expect zlib header */ + if (state.head) { + state.head.done = false; + } + if (!(state.wrap & 1) || /* check if zlib header allowed */ + (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) { + strm.msg = 'incorrect header check'; + state.mode = BAD; + break; + } + if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// + len = (hold & 0x0f)/*BITS(4)*/ + 8; + if (state.wbits === 0) { + state.wbits = len; + } + else if (len > state.wbits) { + strm.msg = 'invalid window size'; + state.mode = BAD; + break; + } + state.dmax = 1 << len; + //Tracev((stderr, "inflate: zlib header ok\n")); + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = hold & 0x200 ? DICTID : TYPE; + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + break; + case FLAGS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.flags = hold; + if ((state.flags & 0xff) !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + if (state.flags & 0xe000) { + strm.msg = 'unknown header flags set'; + state.mode = BAD; + break; + } + if (state.head) { + state.head.text = ((hold >> 8) & 1); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = TIME; + /* falls through */ + case TIME: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.time = hold; + } + if (state.flags & 0x0200) { + //=== CRC4(state.check, hold) + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + hbuf[2] = (hold >>> 16) & 0xff; + hbuf[3] = (hold >>> 24) & 0xff; + state.check = crc32(state.check, hbuf, 4, 0); + //=== + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = OS; + /* falls through */ + case OS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.xflags = (hold & 0xff); + state.head.os = (hold >> 8); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = EXLEN; + /* falls through */ + case EXLEN: + if (state.flags & 0x0400) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length = hold; + if (state.head) { + state.head.extra_len = hold; + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + else if (state.head) { + state.head.extra = null/*Z_NULL*/; + } + state.mode = EXTRA; + /* falls through */ + case EXTRA: + if (state.flags & 0x0400) { + copy = state.length; + if (copy > have) { copy = have; } + if (copy) { + if (state.head) { + len = state.head.extra_len - state.length; + if (!state.head.extra) { + // Use untyped array for more conveniend processing later + state.head.extra = new Array(state.head.extra_len); + } + utils.arraySet( + state.head.extra, + input, + next, + // extra field is limited to 65536 bytes + // - no need for additional size check + copy, + /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/ + len + ); + //zmemcpy(state.head.extra + len, next, + // len + copy > state.head.extra_max ? + // state.head.extra_max - len : copy); + } + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + state.length -= copy; + } + if (state.length) { break inf_leave; } + } + state.length = 0; + state.mode = NAME; + /* falls through */ + case NAME: + if (state.flags & 0x0800) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + // TODO: 2 or 1 bytes? + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.name_max*/)) { + state.head.name += String.fromCharCode(len); + } + } while (len && copy < have); + + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.name = null; + } + state.length = 0; + state.mode = COMMENT; + /* falls through */ + case COMMENT: + if (state.flags & 0x1000) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.comm_max*/)) { + state.head.comment += String.fromCharCode(len); + } + } while (len && copy < have); + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.comment = null; + } + state.mode = HCRC; + /* falls through */ + case HCRC: + if (state.flags & 0x0200) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.check & 0xffff)) { + strm.msg = 'header crc mismatch'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + if (state.head) { + state.head.hcrc = ((state.flags >> 9) & 1); + state.head.done = true; + } + strm.adler = state.check = 0 /*crc32(0L, Z_NULL, 0)*/; + state.mode = TYPE; + break; + case DICTID: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + strm.adler = state.check = ZSWAP32(hold); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = DICT; + /* falls through */ + case DICT: + if (state.havedict === 0) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + return Z_NEED_DICT; + } + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = TYPE; + /* falls through */ + case TYPE: + if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; } + /* falls through */ + case TYPEDO: + if (state.last) { + //--- BYTEBITS() ---// + hold >>>= bits & 7; + bits -= bits & 7; + //---// + state.mode = CHECK; + break; + } + //=== NEEDBITS(3); */ + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.last = (hold & 0x01)/*BITS(1)*/; + //--- DROPBITS(1) ---// + hold >>>= 1; + bits -= 1; + //---// + + switch ((hold & 0x03)/*BITS(2)*/) { + case 0: /* stored block */ + //Tracev((stderr, "inflate: stored block%s\n", + // state.last ? " (last)" : "")); + state.mode = STORED; + break; + case 1: /* fixed block */ + fixedtables(state); + //Tracev((stderr, "inflate: fixed codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = LEN_; /* decode codes */ + if (flush === Z_TREES) { + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break inf_leave; + } + break; + case 2: /* dynamic block */ + //Tracev((stderr, "inflate: dynamic codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = TABLE; + break; + case 3: + strm.msg = 'invalid block type'; + state.mode = BAD; + } + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break; + case STORED: + //--- BYTEBITS() ---// /* go to byte boundary */ + hold >>>= bits & 7; + bits -= bits & 7; + //---// + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) { + strm.msg = 'invalid stored block lengths'; + state.mode = BAD; + break; + } + state.length = hold & 0xffff; + //Tracev((stderr, "inflate: stored length %u\n", + // state.length)); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = COPY_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case COPY_: + state.mode = COPY; + /* falls through */ + case COPY: + copy = state.length; + if (copy) { + if (copy > have) { copy = have; } + if (copy > left) { copy = left; } + if (copy === 0) { break inf_leave; } + //--- zmemcpy(put, next, copy); --- + utils.arraySet(output, input, next, copy, put); + //---// + have -= copy; + next += copy; + left -= copy; + put += copy; + state.length -= copy; + break; + } + //Tracev((stderr, "inflate: stored end\n")); + state.mode = TYPE; + break; + case TABLE: + //=== NEEDBITS(14); */ + while (bits < 14) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4; + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// +//#ifndef PKZIP_BUG_WORKAROUND + if (state.nlen > 286 || state.ndist > 30) { + strm.msg = 'too many length or distance symbols'; + state.mode = BAD; + break; + } +//#endif + //Tracev((stderr, "inflate: table sizes ok\n")); + state.have = 0; + state.mode = LENLENS; + /* falls through */ + case LENLENS: + while (state.have < state.ncode) { + //=== NEEDBITS(3); + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.lens[order[state.have++]] = (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + while (state.have < 19) { + state.lens[order[state.have++]] = 0; + } + // We have separate tables & no pointers. 2 commented lines below not needed. + //state.next = state.codes; + //state.lencode = state.next; + // Switch to use dynamic table + state.lencode = state.lendyn; + state.lenbits = 7; + + opts = {bits: state.lenbits}; + ret = inflate_table(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts); + state.lenbits = opts.bits; + + if (ret) { + strm.msg = 'invalid code lengths set'; + state.mode = BAD; + break; + } + //Tracev((stderr, "inflate: code lengths ok\n")); + state.have = 0; + state.mode = CODELENS; + /* falls through */ + case CODELENS: + while (state.have < state.nlen + state.ndist) { + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_val < 16) { + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.lens[state.have++] = here_val; + } + else { + if (here_val === 16) { + //=== NEEDBITS(here.bits + 2); + n = here_bits + 2; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + if (state.have === 0) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + len = state.lens[state.have - 1]; + copy = 3 + (hold & 0x03);//BITS(2); + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + } + else if (here_val === 17) { + //=== NEEDBITS(here.bits + 3); + n = here_bits + 3; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 3 + (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + else { + //=== NEEDBITS(here.bits + 7); + n = here_bits + 7; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 11 + (hold & 0x7f);//BITS(7); + //--- DROPBITS(7) ---// + hold >>>= 7; + bits -= 7; + //---// + } + if (state.have + copy > state.nlen + state.ndist) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + while (copy--) { + state.lens[state.have++] = len; + } + } + } + + /* handle error breaks in while */ + if (state.mode === BAD) { break; } + + /* check for end-of-block code (better have one) */ + if (state.lens[256] === 0) { + strm.msg = 'invalid code -- missing end-of-block'; + state.mode = BAD; + break; + } + + /* build code tables -- note: do not change the lenbits or distbits + values here (9 and 6) without reading the comments in inftrees.h + concerning the ENOUGH constants, which depend on those values */ + state.lenbits = 9; + + opts = {bits: state.lenbits}; + ret = inflate_table(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.lenbits = opts.bits; + // state.lencode = state.next; + + if (ret) { + strm.msg = 'invalid literal/lengths set'; + state.mode = BAD; + break; + } + + state.distbits = 6; + //state.distcode.copy(state.codes); + // Switch to use dynamic table + state.distcode = state.distdyn; + opts = {bits: state.distbits}; + ret = inflate_table(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.distbits = opts.bits; + // state.distcode = state.next; + + if (ret) { + strm.msg = 'invalid distances set'; + state.mode = BAD; + break; + } + //Tracev((stderr, 'inflate: codes ok\n')); + state.mode = LEN_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case LEN_: + state.mode = LEN; + /* falls through */ + case LEN: + if (have >= 6 && left >= 258) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + inflate_fast(strm, _out); + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + if (state.mode === TYPE) { + state.back = -1; + } + break; + } + state.back = 0; + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) -1)]; /*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if (here_bits <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_op && (here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.lencode[last_val + + ((hold & ((1 << (last_bits + last_op)) -1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + state.length = here_val; + if (here_op === 0) { + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + state.mode = LIT; + break; + } + if (here_op & 32) { + //Tracevv((stderr, "inflate: end of block\n")); + state.back = -1; + state.mode = TYPE; + break; + } + if (here_op & 64) { + strm.msg = 'invalid literal/length code'; + state.mode = BAD; + break; + } + state.extra = here_op & 15; + state.mode = LENEXT; + /* falls through */ + case LENEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length += hold & ((1 << state.extra) -1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } + //Tracevv((stderr, "inflate: length %u\n", state.length)); + state.was = state.length; + state.mode = DIST; + /* falls through */ + case DIST: + for (;;) { + here = state.distcode[hold & ((1 << state.distbits) -1)];/*BITS(state.distbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if ((here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.distcode[last_val + + ((hold & ((1 << (last_bits + last_op)) -1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + if (here_op & 64) { + strm.msg = 'invalid distance code'; + state.mode = BAD; + break; + } + state.offset = here_val; + state.extra = (here_op) & 15; + state.mode = DISTEXT; + /* falls through */ + case DISTEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.offset += hold & ((1 << state.extra) -1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } +//#ifdef INFLATE_STRICT + if (state.offset > state.dmax) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } +//#endif + //Tracevv((stderr, "inflate: distance %u\n", state.offset)); + state.mode = MATCH; + /* falls through */ + case MATCH: + if (left === 0) { break inf_leave; } + copy = _out - left; + if (state.offset > copy) { /* copy from window */ + copy = state.offset - copy; + if (copy > state.whave) { + if (state.sane) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } +// (!) This block is disabled in zlib defailts, +// don't enable it for binary compatibility +//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR +// Trace((stderr, "inflate.c too far\n")); +// copy -= state.whave; +// if (copy > state.length) { copy = state.length; } +// if (copy > left) { copy = left; } +// left -= copy; +// state.length -= copy; +// do { +// output[put++] = 0; +// } while (--copy); +// if (state.length === 0) { state.mode = LEN; } +// break; +//#endif + } + if (copy > state.wnext) { + copy -= state.wnext; + from = state.wsize - copy; + } + else { + from = state.wnext - copy; + } + if (copy > state.length) { copy = state.length; } + from_source = state.window; + } + else { /* copy from output */ + from_source = output; + from = put - state.offset; + copy = state.length; + } + if (copy > left) { copy = left; } + left -= copy; + state.length -= copy; + do { + output[put++] = from_source[from++]; + } while (--copy); + if (state.length === 0) { state.mode = LEN; } + break; + case LIT: + if (left === 0) { break inf_leave; } + output[put++] = state.length; + left--; + state.mode = LEN; + break; + case CHECK: + if (state.wrap) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + // Use '|' insdead of '+' to make sure that result is signed + hold |= input[next++] << bits; + bits += 8; + } + //===// + _out -= left; + strm.total_out += _out; + state.total += _out; + if (_out) { + strm.adler = state.check = + /*UPDATE(state.check, put - _out, _out);*/ + (state.flags ? crc32(state.check, output, _out, put - _out) : adler32(state.check, output, _out, put - _out)); + + } + _out = left; + // NB: crc32 stored as signed 32-bit int, ZSWAP32 returns signed too + if ((state.flags ? hold : ZSWAP32(hold)) !== state.check) { + strm.msg = 'incorrect data check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: check matches trailer\n")); + } + state.mode = LENGTH; + /* falls through */ + case LENGTH: + if (state.wrap && state.flags) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.total & 0xffffffff)) { + strm.msg = 'incorrect length check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: length matches trailer\n")); + } + state.mode = DONE; + /* falls through */ + case DONE: + ret = Z_STREAM_END; + break inf_leave; + case BAD: + ret = Z_DATA_ERROR; + break inf_leave; + case MEM: + return Z_MEM_ERROR; + case SYNC: + /* falls through */ + default: + return Z_STREAM_ERROR; + } + } + + // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave" + + /* + Return from inflate(), updating the total counts and the check value. + If there was no progress during the inflate() call, return a buffer + error. Call updatewindow() to create and/or update the window state. + Note: a memory error from inflate() is non-recoverable. + */ + + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + + if (state.wsize || (_out !== strm.avail_out && state.mode < BAD && + (state.mode < CHECK || flush !== Z_FINISH))) { + if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) { + state.mode = MEM; + return Z_MEM_ERROR; + } + } + _in -= strm.avail_in; + _out -= strm.avail_out; + strm.total_in += _in; + strm.total_out += _out; + state.total += _out; + if (state.wrap && _out) { + strm.adler = state.check = /*UPDATE(state.check, strm.next_out - _out, _out);*/ + (state.flags ? crc32(state.check, output, _out, strm.next_out - _out) : adler32(state.check, output, _out, strm.next_out - _out)); + } + strm.data_type = state.bits + (state.last ? 64 : 0) + + (state.mode === TYPE ? 128 : 0) + + (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0); + if (((_in === 0 && _out === 0) || flush === Z_FINISH) && ret === Z_OK) { + ret = Z_BUF_ERROR; + } + return ret; +} + +function inflateEnd(strm) { + + if (!strm || !strm.state /*|| strm->zfree == (free_func)0*/) { + return Z_STREAM_ERROR; + } + + var state = strm.state; + if (state.window) { + state.window = null; + } + strm.state = null; + return Z_OK; +} + +function inflateGetHeader(strm, head) { + var state; + + /* check state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR; } + + /* save header structure */ + state.head = head; + head.done = false; + return Z_OK; +} + + +exports.inflateReset = inflateReset; +exports.inflateReset2 = inflateReset2; +exports.inflateResetKeep = inflateResetKeep; +exports.inflateInit = inflateInit; +exports.inflateInit2 = inflateInit2; +exports.inflate = inflate; +exports.inflateEnd = inflateEnd; +exports.inflateGetHeader = inflateGetHeader; +exports.inflateInfo = 'pako inflate (from Nodeca project)'; + +/* Not implemented +exports.inflateCopy = inflateCopy; +exports.inflateGetDictionary = inflateGetDictionary; +exports.inflateMark = inflateMark; +exports.inflatePrime = inflatePrime; +exports.inflateSetDictionary = inflateSetDictionary; +exports.inflateSync = inflateSync; +exports.inflateSyncPoint = inflateSyncPoint; +exports.inflateUndermine = inflateUndermine; +*/ + +},{"../utils/common":1,"./adler32":2,"./crc32":3,"./inffast":4,"./inftrees":6}],6:[function(require,module,exports){ +'use strict'; + + +var utils = require('../utils/common'); + +var MAXBITS = 15; +var ENOUGH_LENS = 852; +var ENOUGH_DISTS = 592; +//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS); + +var CODES = 0; +var LENS = 1; +var DISTS = 2; + +var lbase = [ /* Length codes 257..285 base */ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 +]; + +var lext = [ /* Length codes 257..285 extra */ + 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, + 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78 +]; + +var dbase = [ /* Distance codes 0..29 base */ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577, 0, 0 +]; + +var dext = [ /* Distance codes 0..29 extra */ + 16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, + 28, 28, 29, 29, 64, 64 +]; + +module.exports = function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts) +{ + var bits = opts.bits; + //here = opts.here; /* table entry for duplication */ + + var len = 0; /* a code's length in bits */ + var sym = 0; /* index of code symbols */ + var min = 0, max = 0; /* minimum and maximum code lengths */ + var root = 0; /* number of index bits for root table */ + var curr = 0; /* number of index bits for current table */ + var drop = 0; /* code bits to drop for sub-table */ + var left = 0; /* number of prefix codes available */ + var used = 0; /* code entries in table used */ + var huff = 0; /* Huffman code */ + var incr; /* for incrementing code, index */ + var fill; /* index for replicating entries */ + var low; /* low bits for current root entry */ + var mask; /* mask for low root bits */ + var next; /* next available space in table */ + var base = null; /* base value table to use */ + var base_index = 0; +// var shoextra; /* extra bits table to use */ + var end; /* use base and extra for symbol > end */ + var count = new utils.Buf16(MAXBITS+1); //[MAXBITS+1]; /* number of codes of each length */ + var offs = new utils.Buf16(MAXBITS+1); //[MAXBITS+1]; /* offsets in table for each length */ + var extra = null; + var extra_index = 0; + + var here_bits, here_op, here_val; + + /* + Process a set of code lengths to create a canonical Huffman code. The + code lengths are lens[0..codes-1]. Each length corresponds to the + symbols 0..codes-1. The Huffman code is generated by first sorting the + symbols by length from short to long, and retaining the symbol order + for codes with equal lengths. Then the code starts with all zero bits + for the first code of the shortest length, and the codes are integer + increments for the same length, and zeros are appended as the length + increases. For the deflate format, these bits are stored backwards + from their more natural integer increment ordering, and so when the + decoding tables are built in the large loop below, the integer codes + are incremented backwards. + + This routine assumes, but does not check, that all of the entries in + lens[] are in the range 0..MAXBITS. The caller must assure this. + 1..MAXBITS is interpreted as that code length. zero means that that + symbol does not occur in this code. + + The codes are sorted by computing a count of codes for each length, + creating from that a table of starting indices for each length in the + sorted table, and then entering the symbols in order in the sorted + table. The sorted table is work[], with that space being provided by + the caller. + + The length counts are used for other purposes as well, i.e. finding + the minimum and maximum length codes, determining if there are any + codes at all, checking for a valid set of lengths, and looking ahead + at length counts to determine sub-table sizes when building the + decoding tables. + */ + + /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */ + for (len = 0; len <= MAXBITS; len++) { + count[len] = 0; + } + for (sym = 0; sym < codes; sym++) { + count[lens[lens_index + sym]]++; + } + + /* bound code lengths, force root to be within code lengths */ + root = bits; + for (max = MAXBITS; max >= 1; max--) { + if (count[max] !== 0) { break; } + } + if (root > max) { + root = max; + } + if (max === 0) { /* no symbols to code at all */ + //table.op[opts.table_index] = 64; //here.op = (var char)64; /* invalid code marker */ + //table.bits[opts.table_index] = 1; //here.bits = (var char)1; + //table.val[opts.table_index++] = 0; //here.val = (var short)0; + table[table_index++] = (1 << 24) | (64 << 16) | 0; + + + //table.op[opts.table_index] = 64; + //table.bits[opts.table_index] = 1; + //table.val[opts.table_index++] = 0; + table[table_index++] = (1 << 24) | (64 << 16) | 0; + + opts.bits = 1; + return 0; /* no symbols, but wait for decoding to report error */ + } + for (min = 1; min < max; min++) { + if (count[min] !== 0) { break; } + } + if (root < min) { + root = min; + } + + /* check for an over-subscribed or incomplete set of lengths */ + left = 1; + for (len = 1; len <= MAXBITS; len++) { + left <<= 1; + left -= count[len]; + if (left < 0) { + return -1; + } /* over-subscribed */ + } + if (left > 0 && (type === CODES || max !== 1)) { + return -1; /* incomplete set */ + } + + /* generate offsets into symbol table for each length for sorting */ + offs[1] = 0; + for (len = 1; len < MAXBITS; len++) { + offs[len + 1] = offs[len] + count[len]; + } + + /* sort symbols by length, by symbol order within each length */ + for (sym = 0; sym < codes; sym++) { + if (lens[lens_index + sym] !== 0) { + work[offs[lens[lens_index + sym]]++] = sym; + } + } + + /* + Create and fill in decoding tables. In this loop, the table being + filled is at next and has curr index bits. The code being used is huff + with length len. That code is converted to an index by dropping drop + bits off of the bottom. For codes where len is less than drop + curr, + those top drop + curr - len bits are incremented through all values to + fill the table with replicated entries. + + root is the number of index bits for the root table. When len exceeds + root, sub-tables are created pointed to by the root entry with an index + of the low root bits of huff. This is saved in low to check for when a + new sub-table should be started. drop is zero when the root table is + being filled, and drop is root when sub-tables are being filled. + + When a new sub-table is needed, it is necessary to look ahead in the + code lengths to determine what size sub-table is needed. The length + counts are used for this, and so count[] is decremented as codes are + entered in the tables. + + used keeps track of how many table entries have been allocated from the + provided *table space. It is checked for LENS and DIST tables against + the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in + the initial root table size constants. See the comments in inftrees.h + for more information. + + sym increments through all symbols, and the loop terminates when + all codes of length max, i.e. all codes, have been processed. This + routine permits incomplete codes, so another loop after this one fills + in the rest of the decoding tables with invalid code markers. + */ + + /* set up for code type */ + // poor man optimization - use if-else instead of switch, + // to avoid deopts in old v8 + if (type === CODES) { + base = extra = work; /* dummy value--not used */ + end = 19; + + } else if (type === LENS) { + base = lbase; + base_index -= 257; + extra = lext; + extra_index -= 257; + end = 256; + + } else { /* DISTS */ + base = dbase; + extra = dext; + end = -1; + } + + /* initialize opts for loop */ + huff = 0; /* starting code */ + sym = 0; /* starting code symbol */ + len = min; /* starting code length */ + next = table_index; /* current table to fill in */ + curr = root; /* current table index bits */ + drop = 0; /* current bits to drop from code for index */ + low = -1; /* trigger new sub-table when len > root */ + used = 1 << root; /* use root table entries */ + mask = used - 1; /* mask for comparing low */ + + /* check available table space */ + if ((type === LENS && used > ENOUGH_LENS) || + (type === DISTS && used > ENOUGH_DISTS)) { + return 1; + } + + var i=0; + /* process all codes and make table entries */ + for (;;) { + i++; + /* create table entry */ + here_bits = len - drop; + if (work[sym] < end) { + here_op = 0; + here_val = work[sym]; + } + else if (work[sym] > end) { + here_op = extra[extra_index + work[sym]]; + here_val = base[base_index + work[sym]]; + } + else { + here_op = 32 + 64; /* end of block */ + here_val = 0; + } + + /* replicate for those indices with low len bits equal to huff */ + incr = 1 << (len - drop); + fill = 1 << curr; + min = fill; /* save offset to next table */ + do { + fill -= incr; + table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0; + } while (fill !== 0); + + /* backwards increment the len-bit code huff */ + incr = 1 << (len - 1); + while (huff & incr) { + incr >>= 1; + } + if (incr !== 0) { + huff &= incr - 1; + huff += incr; + } else { + huff = 0; + } + + /* go to next symbol, update count, len */ + sym++; + if (--count[len] === 0) { + if (len === max) { break; } + len = lens[lens_index + work[sym]]; + } + + /* create new sub-table if needed */ + if (len > root && (huff & mask) !== low) { + /* if first time, transition to sub-tables */ + if (drop === 0) { + drop = root; + } + + /* increment past last table */ + next += min; /* here min is 1 << curr */ + + /* determine length of next table */ + curr = len - drop; + left = 1 << curr; + while (curr + drop < max) { + left -= count[curr + drop]; + if (left <= 0) { break; } + curr++; + left <<= 1; + } + + /* check for enough space */ + used += 1 << curr; + if ((type === LENS && used > ENOUGH_LENS) || + (type === DISTS && used > ENOUGH_DISTS)) { + return 1; + } + + /* point entry in root table to sub-table */ + low = huff & mask; + /*table.op[low] = curr; + table.bits[low] = root; + table.val[low] = next - opts.table_index;*/ + table[low] = (root << 24) | (curr << 16) | (next - table_index) |0; + } + } + + /* fill in remaining table entry if code is incomplete (guaranteed to have + at most one remaining entry, since if the code is incomplete, the + maximum code length that was allowed to get this far is one bit) */ + if (huff !== 0) { + //table.op[next + huff] = 64; /* invalid code marker */ + //table.bits[next + huff] = len - drop; + //table.val[next + huff] = 0; + table[next + huff] = ((len - drop) << 24) | (64 << 16) |0; + } + + /* set return parameters */ + //opts.table_index += used; + opts.bits = root; + return 0; +}; + +},{"../utils/common":1}],7:[function(require,module,exports){ +'use strict'; + + +function ZStream() { + /* next input byte */ + this.input = null; // JS specific, because we have no pointers + this.next_in = 0; + /* number of bytes available at input */ + this.avail_in = 0; + /* total number of input bytes read so far */ + this.total_in = 0; + /* next output byte should be put there */ + this.output = null; // JS specific, because we have no pointers + this.next_out = 0; + /* remaining free space at output */ + this.avail_out = 0; + /* total number of bytes output so far */ + this.total_out = 0; + /* last error message, NULL if no error */ + this.msg = ''/*Z_NULL*/; + /* not visible by applications */ + this.state = null; + /* best guess about the data type: binary or text */ + this.data_type = 2/*Z_UNKNOWN*/; + /* adler32 value of the uncompressed data */ + this.adler = 0; +} + +module.exports = ZStream; + +},{}],8:[function(require,module,exports){ +var zlib = require('../node_modules/pako/lib/zlib/inflate.js'); +var ZStream = require('../node_modules/pako/lib/zlib/zstream.js'); + +var Inflate = function () { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + zlib.inflateInit(this.strm, this.windowBits); +}; + +Inflate.prototype = { + inflate: function (data, flush, expected) { + this.strm.input = data; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.next_out = 0; + + // resize our output buffer if it's too small + // (we could just use multiple chunks, but that would cause an extra + // allocation each time to flatten the chunks) + if (expected > this.chunkSize) { + this.chunkSize = expected; + this.strm.output = new Uint8Array(this.chunkSize); + } + + this.strm.avail_out = this.chunkSize; + + zlib.inflate(this.strm, flush); + + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + }, + + reset: function () { + zlib.inflateReset(this.strm); + } +}; + +module.exports = {Inflate: Inflate}; + +},{"../node_modules/pako/lib/zlib/inflate.js":5,"../node_modules/pako/lib/zlib/zstream.js":7}]},{},[8])(8) +}); \ No newline at end of file diff --git a/public/novnc/include/input.js b/public/novnc/include/input.js new file mode 100644 index 00000000..fa6ba44a --- /dev/null +++ b/public/novnc/include/input.js @@ -0,0 +1,389 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/*jslint browser: true, white: false */ +/*global window, Util */ + +var Keyboard, Mouse; + +(function () { + "use strict"; + + // + // Keyboard event handler + // + + Keyboard = function (defaults) { + this._keyDownList = []; // List of depressed keys + // (even if they are happy) + + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true + }); + + // create the keyboard handler + this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(), + VerifyCharModifier( /* jshint newcap: false */ + TrackKeyState( + EscapeModifiers(this._handleRfbEvent.bind(this)) + ) + ) + ); /* jshint newcap: true */ + + // keep these here so we can refer to them later + this._eventHandlers = { + 'keyup': this._handleKeyUp.bind(this), + 'keydown': this._handleKeyDown.bind(this), + 'keypress': this._handleKeyPress.bind(this), + 'blur': this._allKeysUp.bind(this) + }; + }; + + Keyboard.prototype = { + // private methods + + _handleRfbEvent: function (e) { + if (this._onKeyPress) { + Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + + ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); + this._onKeyPress(e.keysym.keysym, e.type == 'keydown'); + } + }, + + _handleKeyDown: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keydown(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _handleKeyPress: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keypress(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _handleKeyUp: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keyup(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _allKeysUp: function () { + Util.Debug(">> Keyboard.allKeysUp"); + this._handler.releaseAll(); + Util.Debug("<< Keyboard.allKeysUp"); + }, + + // Public methods + + grab: function () { + //Util.Debug(">> Keyboard.grab"); + var c = this._target; + + Util.addEvent(c, 'keydown', this._eventHandlers.keydown); + Util.addEvent(c, 'keyup', this._eventHandlers.keyup); + Util.addEvent(c, 'keypress', this._eventHandlers.keypress); + + // Release (key up) if window loses focus + Util.addEvent(window, 'blur', this._eventHandlers.blur); + + //Util.Debug("<< Keyboard.grab"); + }, + + ungrab: function () { + //Util.Debug(">> Keyboard.ungrab"); + var c = this._target; + + Util.removeEvent(c, 'keydown', this._eventHandlers.keydown); + Util.removeEvent(c, 'keyup', this._eventHandlers.keyup); + Util.removeEvent(c, 'keypress', this._eventHandlers.keypress); + Util.removeEvent(window, 'blur', this._eventHandlers.blur); + + // Release (key up) all keys that are in a down state + this._allKeysUp(); + + //Util.Debug(">> Keyboard.ungrab"); + }, + + sync: function (e) { + this._handler.syncModifiers(e); + } + }; + + Util.make_properties(Keyboard, [ + ['target', 'wo', 'dom'], // DOM element that captures keyboard input + ['focused', 'rw', 'bool'], // Capture and send key events + + ['onKeyPress', 'rw', 'func'] // Handler for key press/release + ]); + + // + // Mouse event handler + // + + Mouse = function (defaults) { + this._mouseCaptured = false; + + this._doubleClickTimer = null; + this._lastTouchPos = null; + + // Configuration attributes + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true, + 'scale': 1.0, + 'touchButton': 1 + }); + + this._eventHandlers = { + 'mousedown': this._handleMouseDown.bind(this), + 'mouseup': this._handleMouseUp.bind(this), + 'mousemove': this._handleMouseMove.bind(this), + 'mousewheel': this._handleMouseWheel.bind(this), + 'mousedisable': this._handleMouseDisable.bind(this) + }; + }; + + Mouse.prototype = { + // private methods + _captureMouse: function () { + // capturing the mouse ensures we get the mouseup event + if (this._target.setCapture) { + this._target.setCapture(); + } + + // some browsers give us mouseup events regardless, + // so if we never captured the mouse, we can disregard the event + this._mouseCaptured = true; + }, + + _releaseMouse: function () { + if (this._target.releaseCapture) { + this._target.releaseCapture(); + } + this._mouseCaptured = false; + }, + + _resetDoubleClickTimer: function () { + this._doubleClickTimer = null; + }, + + _handleMouseButton: function (e, down) { + if (!this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + var bmask; + if (e.touches || e.changedTouches) { + // Touch device + + // When two touches occur within 500 ms of each other and are + // close enough together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + var xs = this._lastTouchPos.x - pos.x; + var ys = this._lastTouchPos.y - pos.y; + var d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var threshold = 20 * (window.devicePixelRatio || 1); + if (d < threshold) { + pos = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this._touchButton; + // If bmask is set + } else if (evt.which) { + /* everything except IE */ + bmask = 1 << evt.button; + } else { + /* IE including 9 */ + bmask = (evt.button & 0x1) + // Left + (evt.button & 0x2) * 2 + // Right + (evt.button & 0x4) / 2; // Middle + } + + if (this._onMouseButton) { + Util.Debug("onMouseButton " + (down ? "down" : "up") + + ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); + this._onMouseButton(pos.x, pos.y, down, bmask); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDown: function (e) { + this._captureMouse(); + this._handleMouseButton(e, 1); + }, + + _handleMouseUp: function (e) { + if (!this._mouseCaptured) { return; } + + this._handleMouseButton(e, 0); + this._releaseMouse(); + }, + + _handleMouseWheel: function (e) { + if (!this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; + var bmask; + if (wheelData > 0) { + bmask = 1 << 3; + } else { + bmask = 1 << 4; + } + + if (this._onMouseButton) { + this._onMouseButton(pos.x, pos.y, 1, bmask); + this._onMouseButton(pos.x, pos.y, 0, bmask); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseMove: function (e) { + if (! this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + if (this._onMouseMove) { + this._onMouseMove(pos.x, pos.y); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDisable: function (e) { + if (!this._focused) { return true; } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + /* Stop propagation if inside canvas area */ + if ((pos.realx >= 0) && (pos.realy >= 0) && + (pos.realx < this._target.offsetWidth) && + (pos.realy < this._target.offsetHeight)) { + //Util.Debug("mouse event disabled"); + Util.stopEvent(e); + return false; + } + + return true; + }, + + + // Public methods + grab: function () { + var c = this._target; + + if ('ontouchstart' in document.documentElement) { + Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.addEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } + + /* Work around right and middle click browser behaviors */ + Util.addEvent(document, 'click', this._eventHandlers.mousedisable); + Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + }, + + ungrab: function () { + var c = this._target; + + if ('ontouchstart' in document.documentElement) { + Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } + + /* Work around right and middle click browser behaviors */ + Util.removeEvent(document, 'click', this._eventHandlers.mousedisable); + Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + + } + }; + + Util.make_properties(Mouse, [ + ['target', 'ro', 'dom'], // DOM element that captures mouse input + ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received + ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement + ['scale', 'rw', 'float'], // Viewport scale factor 0.0 - 1.0 + + ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release + ['onMouseMove', 'rw', 'func'], // Handler for mouse movement + ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + ]); +})(); diff --git a/public/novnc/include/keyboard.js b/public/novnc/include/keyboard.js new file mode 100644 index 00000000..86670312 --- /dev/null +++ b/public/novnc/include/keyboard.js @@ -0,0 +1,543 @@ +var kbdUtil = (function() { + "use strict"; + + function substituteCodepoint(cp) { + // Any Unicode code points which do not have corresponding keysym entries + // can be swapped out for another code point by adding them to this table + var substitutions = { + // {S,s} with comma below -> {S,s} with cedilla + 0x218 : 0x15e, + 0x219 : 0x15f, + // {T,t} with comma below -> {T,t} with cedilla + 0x21a : 0x162, + 0x21b : 0x163 + }; + + var sub = substitutions[cp]; + return sub ? sub : cp; + } + + function isMac() { + return navigator && !!(/mac/i).exec(navigator.platform); + } + function isWindows() { + return navigator && !!(/win/i).exec(navigator.platform); + } + function isLinux() { + return navigator && !!(/linux/i).exec(navigator.platform); + } + + // Return true if a modifier which is not the specified char modifier (and is not shift) is down + function hasShortcutModifier(charModifier, currentModifiers) { + var mods = {}; + for (var key in currentModifiers) { + if (parseInt(key) !== XK_Shift_L) { + mods[key] = currentModifiers[key]; + } + } + + var sum = 0; + for (var k in currentModifiers) { + if (mods[k]) { + ++sum; + } + } + if (hasCharModifier(charModifier, mods)) { + return sum > charModifier.length; + } + else { + return sum > 0; + } + } + + // Return true if the specified char modifier is currently down + function hasCharModifier(charModifier, currentModifiers) { + if (charModifier.length === 0) { return false; } + + for (var i = 0; i < charModifier.length; ++i) { + if (!currentModifiers[charModifier[i]]) { + return false; + } + } + return true; + } + + // Helper object tracking modifier key state + // and generates fake key events to compensate if it gets out of sync + function ModifierSync(charModifier) { + if (!charModifier) { + if (isMac()) { + // on Mac, Option (AKA Alt) is used as a char modifier + charModifier = [XK_Alt_L]; + } + else if (isWindows()) { + // on Windows, Ctrl+Alt is used as a char modifier + charModifier = [XK_Alt_L, XK_Control_L]; + } + else if (isLinux()) { + // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier + charModifier = [XK_ISO_Level3_Shift]; + } + else { + charModifier = []; + } + } + + var state = {}; + state[XK_Control_L] = false; + state[XK_Alt_L] = false; + state[XK_ISO_Level3_Shift] = false; + state[XK_Shift_L] = false; + state[XK_Meta_L] = false; + + function sync(evt, keysym) { + var result = []; + function syncKey(keysym) { + return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; + } + + if (evt.ctrlKey !== undefined && + evt.ctrlKey !== state[XK_Control_L] && keysym !== XK_Control_L) { + state[XK_Control_L] = evt.ctrlKey; + result.push(syncKey(XK_Control_L)); + } + if (evt.altKey !== undefined && + evt.altKey !== state[XK_Alt_L] && keysym !== XK_Alt_L) { + state[XK_Alt_L] = evt.altKey; + result.push(syncKey(XK_Alt_L)); + } + if (evt.altGraphKey !== undefined && + evt.altGraphKey !== state[XK_ISO_Level3_Shift] && keysym !== XK_ISO_Level3_Shift) { + state[XK_ISO_Level3_Shift] = evt.altGraphKey; + result.push(syncKey(XK_ISO_Level3_Shift)); + } + if (evt.shiftKey !== undefined && + evt.shiftKey !== state[XK_Shift_L] && keysym !== XK_Shift_L) { + state[XK_Shift_L] = evt.shiftKey; + result.push(syncKey(XK_Shift_L)); + } + if (evt.metaKey !== undefined && + evt.metaKey !== state[XK_Meta_L] && keysym !== XK_Meta_L) { + state[XK_Meta_L] = evt.metaKey; + result.push(syncKey(XK_Meta_L)); + } + return result; + } + function syncKeyEvent(evt, down) { + var obj = getKeysym(evt); + var keysym = obj ? obj.keysym : null; + + // first, apply the event itself, if relevant + if (keysym !== null && state[keysym] !== undefined) { + state[keysym] = down; + } + return sync(evt, keysym); + } + + return { + // sync on the appropriate keyboard event + keydown: function(evt) { return syncKeyEvent(evt, true);}, + keyup: function(evt) { return syncKeyEvent(evt, false);}, + // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway + syncAny: function(evt) { return sync(evt);}, + + // is a shortcut modifier down? + hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, + // if a char modifier is down, return the keys it consists of, otherwise return null + activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } + }; + } + + // Get a key ID from a keyboard event + // May be a string or an integer depending on the available properties + function getKey(evt){ + if ('keyCode' in evt && 'key' in evt) { + return evt.key + ':' + evt.keyCode; + } + else if ('keyCode' in evt) { + return evt.keyCode; + } + else { + return evt.key; + } + } + + // Get the most reliable keysym value we can get from a key event + // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which + function getKeysym(evt){ + var codepoint; + if (evt.char && evt.char.length === 1) { + codepoint = evt.char.charCodeAt(); + } + else if (evt.charCode) { + codepoint = evt.charCode; + } + else if (evt.keyCode && evt.type === 'keypress') { + // IE10 stores the char code as keyCode, and has no other useful properties + codepoint = evt.keyCode; + } + if (codepoint) { + var res = keysyms.fromUnicode(substituteCodepoint(codepoint)); + if (res) { + return res; + } + } + // we could check evt.key here. + // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, + // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key + // so we don't *need* it yet + if (evt.keyCode) { + return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); + } + if (evt.which) { + return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); + } + return null; + } + + // Given a keycode, try to predict which keysym it might be. + // If the keycode is unknown, null is returned. + function keysymFromKeyCode(keycode, shiftPressed) { + if (typeof(keycode) !== 'number') { + return null; + } + // won't be accurate for azerty + if (keycode >= 0x30 && keycode <= 0x39) { + return keycode; // digit + } + if (keycode >= 0x41 && keycode <= 0x5a) { + // remap to lowercase unless shift is down + return shiftPressed ? keycode : keycode + 32; // A-Z + } + if (keycode >= 0x60 && keycode <= 0x69) { + return XK_KP_0 + (keycode - 0x60); // numpad 0-9 + } + + switch(keycode) { + case 0x20: return XK_space; + case 0x6a: return XK_KP_Multiply; + case 0x6b: return XK_KP_Add; + case 0x6c: return XK_KP_Separator; + case 0x6d: return XK_KP_Subtract; + case 0x6e: return XK_KP_Decimal; + case 0x6f: return XK_KP_Divide; + case 0xbb: return XK_plus; + case 0xbc: return XK_comma; + case 0xbd: return XK_minus; + case 0xbe: return XK_period; + } + + return nonCharacterKey({keyCode: keycode}); + } + + // if the key is a known non-character key (any key which doesn't generate character data) + // return its keysym value. Otherwise return null + function nonCharacterKey(evt) { + // evt.key not implemented yet + if (!evt.keyCode) { return null; } + var keycode = evt.keyCode; + + if (keycode >= 0x70 && keycode <= 0x87) { + return XK_F1 + keycode - 0x70; // F1-F24 + } + switch (keycode) { + + case 8 : return XK_BackSpace; + case 13 : return XK_Return; + + case 9 : return XK_Tab; + + case 27 : return XK_Escape; + case 46 : return XK_Delete; + + case 36 : return XK_Home; + case 35 : return XK_End; + case 33 : return XK_Page_Up; + case 34 : return XK_Page_Down; + case 45 : return XK_Insert; + + case 37 : return XK_Left; + case 38 : return XK_Up; + case 39 : return XK_Right; + case 40 : return XK_Down; + + case 16 : return XK_Shift_L; + case 17 : return XK_Control_L; + case 18 : return XK_Alt_L; // also: Option-key on Mac + + case 224 : return XK_Meta_L; + case 225 : return XK_ISO_Level3_Shift; // AltGr + case 91 : return XK_Super_L; // also: Windows-key + case 92 : return XK_Super_R; // also: Windows-key + case 93 : return XK_Menu; // also: Windows-Menu, Command on Mac + default: return null; + } + } + return { + hasShortcutModifier : hasShortcutModifier, + hasCharModifier : hasCharModifier, + ModifierSync : ModifierSync, + getKey : getKey, + getKeysym : getKeysym, + keysymFromKeyCode : keysymFromKeyCode, + nonCharacterKey : nonCharacterKey, + substituteCodepoint : substituteCodepoint + }; +})(); + +// Takes a DOM keyboard event and: +// - determines which keysym it represents +// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) +// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down +// - marks each event with an 'escape' property if a modifier was down which should be "escaped" +// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown +// This information is collected into an object which is passed to the next() function. (one call per event) +function KeyEventDecoder(modifierState, next) { + "use strict"; + function sendAll(evts) { + for (var i = 0; i < evts.length; ++i) { + next(evts[i]); + } + } + function process(evt, type) { + var result = {type: type}; + var keyId = kbdUtil.getKey(evt); + if (keyId) { + result.keyId = keyId; + } + + var keysym = kbdUtil.getKeysym(evt); + + var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); + // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? + // "special" keys like enter, tab or backspace don't send keypress events, + // and some browsers don't send keypresses at all if a modifier is down + if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) { + result.keysym = keysym; + } + + var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; + + // Should we prevent the browser from handling the event? + // Doing so on a keydown (in most browsers) prevents keypress from being generated + // so only do that if we have to. + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt)); + + // If a char modifier is down on a keydown, we need to insert a stall, + // so VerifyCharModifier knows to wait and see if a keypress is comnig + var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt); + + // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) + var active = modifierState.activeCharModifier(); + + // If we have a char modifier down, and we're able to determine a keysym reliably + // then (a) we know to treat the modifier as a char modifier, + // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char. + if (active && keysym) { + var isCharModifier = false; + for (var i = 0; i < active.length; ++i) { + if (active[i] === keysym.keysym) { + isCharModifier = true; + } + } + if (type === 'keypress' && !isCharModifier) { + result.escape = modifierState.activeCharModifier(); + } + } + + if (stall) { + // insert a fake "stall" event + next({type: 'stall'}); + } + next(result); + + return suppress; + } + + return { + keydown: function(evt) { + sendAll(modifierState.keydown(evt)); + return process(evt, 'keydown'); + }, + keypress: function(evt) { + return process(evt, 'keypress'); + }, + keyup: function(evt) { + sendAll(modifierState.keyup(evt)); + return process(evt, 'keyup'); + }, + syncModifiers: function(evt) { + sendAll(modifierState.syncAny(evt)); + }, + releaseAll: function() { next({type: 'releaseall'}); } + }; +} + +// Combines keydown and keypress events where necessary to handle char modifiers. +// On some OS'es, a char modifier is sometimes used as a shortcut modifier. +// For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing +// so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not. +// The only way we can distinguish these cases is to wait and see if a keypress event arrives +// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two +function VerifyCharModifier(next) { + "use strict"; + var queue = []; + var timer = null; + function process() { + if (timer) { + return; + } + + var delayProcess = function () { + clearTimeout(timer); + timer = null; + process(); + }; + + while (queue.length !== 0) { + var cur = queue[0]; + queue = queue.splice(1); + switch (cur.type) { + case 'stall': + // insert a delay before processing available events. + /* jshint loopfunc: true */ + timer = setTimeout(delayProcess, 5); + /* jshint loopfunc: false */ + return; + case 'keydown': + // is the next element a keypress? Then we should merge the two + if (queue.length !== 0 && queue[0].type === 'keypress') { + // Firefox sends keypress even when no char is generated. + // so, if keypress keysym is the same as we'd have guessed from keydown, + // the modifier didn't have any effect, and should not be escaped + if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) { + cur.escape = queue[0].escape; + } + cur.keysym = queue[0].keysym; + queue = queue.splice(1); + } + break; + } + + // swallow stall events, and pass all others to the next stage + if (cur.type !== 'stall') { + next(cur); + } + } + } + return function(evt) { + queue.push(evt); + process(); + }; +} + +// Keeps track of which keys we (and the server) believe are down +// When a keyup is received, match it against this list, to determine the corresponding keysym(s) +// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars +// key repeat events should be merged into a single entry. +// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess +function TrackKeyState(next) { + "use strict"; + var state = []; + + return function (evt) { + var last = state.length !== 0 ? state[state.length-1] : null; + + switch (evt.type) { + case 'keydown': + // insert a new entry if last seen key was different. + if (!last || !evt.keyId || last.keyId !== evt.keyId) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (evt.keysym) { + // make sure last event contains this keysym (a single "logical" keyevent + // can cause multiple key events to be sent to the VNC server) + last.keysyms[evt.keysym.keysym] = evt.keysym; + last.ignoreKeyPress = true; + next(evt); + } + break; + case 'keypress': + if (!last) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (!evt.keysym) { + console.log('keypress with no keysym:', evt); + } + + // If we didn't expect a keypress, and already sent a keydown to the VNC server + // based on the keydown, make sure to skip this event. + if (evt.keysym && !last.ignoreKeyPress) { + last.keysyms[evt.keysym.keysym] = evt.keysym; + evt.type = 'keydown'; + next(evt); + } + break; + case 'keyup': + if (state.length === 0) { + return; + } + var idx = null; + // do we have a matching key tracked as being down? + for (var i = 0; i !== state.length; ++i) { + if (state[i].keyId === evt.keyId) { + idx = i; + break; + } + } + // if we couldn't find a match (it happens), assume it was the last key pressed + if (idx === null) { + idx = state.length - 1; + } + + var item = state.splice(idx, 1)[0]; + // for each keysym tracked by this key entry, clone the current event and override the keysym + var clone = (function(){ + function Clone(){} + return function (obj) { Clone.prototype=obj; return new Clone(); }; + }()); + for (var key in item.keysyms) { + var out = clone(evt); + out.keysym = item.keysyms[key]; + next(out); + } + break; + case 'releaseall': + /* jshint shadow: true */ + for (var i = 0; i < state.length; ++i) { + for (var key in state[i].keysyms) { + var keysym = state[i].keysyms[key]; + next({keyId: 0, keysym: keysym, type: 'keyup'}); + } + } + /* jshint shadow: false */ + state = []; + } + }; +} + +// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), +// then the modifier must be "undone" before sending the @, and "redone" afterwards. +function EscapeModifiers(next) { + "use strict"; + return function(evt) { + if (evt.type !== 'keydown' || evt.escape === undefined) { + next(evt); + return; + } + // undo modifiers + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + // send the character event + next(evt); + // redo modifiers + /* jshint shadow: true */ + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + /* jshint shadow: false */ + }; +} diff --git a/public/novnc/include/keysym.js b/public/novnc/include/keysym.js new file mode 100644 index 00000000..58b107c0 --- /dev/null +++ b/public/novnc/include/keysym.js @@ -0,0 +1,378 @@ +var XK_VoidSymbol = 0xffffff, /* Void symbol */ + +XK_BackSpace = 0xff08, /* Back space, back char */ +XK_Tab = 0xff09, +XK_Linefeed = 0xff0a, /* Linefeed, LF */ +XK_Clear = 0xff0b, +XK_Return = 0xff0d, /* Return, enter */ +XK_Pause = 0xff13, /* Pause, hold */ +XK_Scroll_Lock = 0xff14, +XK_Sys_Req = 0xff15, +XK_Escape = 0xff1b, +XK_Delete = 0xffff, /* Delete, rubout */ + +/* Cursor control & motion */ + +XK_Home = 0xff50, +XK_Left = 0xff51, /* Move left, left arrow */ +XK_Up = 0xff52, /* Move up, up arrow */ +XK_Right = 0xff53, /* Move right, right arrow */ +XK_Down = 0xff54, /* Move down, down arrow */ +XK_Prior = 0xff55, /* Prior, previous */ +XK_Page_Up = 0xff55, +XK_Next = 0xff56, /* Next */ +XK_Page_Down = 0xff56, +XK_End = 0xff57, /* EOL */ +XK_Begin = 0xff58, /* BOL */ + + +/* Misc functions */ + +XK_Select = 0xff60, /* Select, mark */ +XK_Print = 0xff61, +XK_Execute = 0xff62, /* Execute, run, do */ +XK_Insert = 0xff63, /* Insert, insert here */ +XK_Undo = 0xff65, +XK_Redo = 0xff66, /* Redo, again */ +XK_Menu = 0xff67, +XK_Find = 0xff68, /* Find, search */ +XK_Cancel = 0xff69, /* Cancel, stop, abort, exit */ +XK_Help = 0xff6a, /* Help */ +XK_Break = 0xff6b, +XK_Mode_switch = 0xff7e, /* Character set switch */ +XK_script_switch = 0xff7e, /* Alias for mode_switch */ +XK_Num_Lock = 0xff7f, + +/* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ + +XK_KP_Space = 0xff80, /* Space */ +XK_KP_Tab = 0xff89, +XK_KP_Enter = 0xff8d, /* Enter */ +XK_KP_F1 = 0xff91, /* PF1, KP_A, ... */ +XK_KP_F2 = 0xff92, +XK_KP_F3 = 0xff93, +XK_KP_F4 = 0xff94, +XK_KP_Home = 0xff95, +XK_KP_Left = 0xff96, +XK_KP_Up = 0xff97, +XK_KP_Right = 0xff98, +XK_KP_Down = 0xff99, +XK_KP_Prior = 0xff9a, +XK_KP_Page_Up = 0xff9a, +XK_KP_Next = 0xff9b, +XK_KP_Page_Down = 0xff9b, +XK_KP_End = 0xff9c, +XK_KP_Begin = 0xff9d, +XK_KP_Insert = 0xff9e, +XK_KP_Delete = 0xff9f, +XK_KP_Equal = 0xffbd, /* Equals */ +XK_KP_Multiply = 0xffaa, +XK_KP_Add = 0xffab, +XK_KP_Separator = 0xffac, /* Separator, often comma */ +XK_KP_Subtract = 0xffad, +XK_KP_Decimal = 0xffae, +XK_KP_Divide = 0xffaf, + +XK_KP_0 = 0xffb0, +XK_KP_1 = 0xffb1, +XK_KP_2 = 0xffb2, +XK_KP_3 = 0xffb3, +XK_KP_4 = 0xffb4, +XK_KP_5 = 0xffb5, +XK_KP_6 = 0xffb6, +XK_KP_7 = 0xffb7, +XK_KP_8 = 0xffb8, +XK_KP_9 = 0xffb9, + +/* + * Auxiliary functions; note the duplicate definitions for left and right + * function keys; Sun keyboards and a few other manufacturers have such + * function key groups on the left and/or right sides of the keyboard. + * We've not found a keyboard with more than 35 function keys total. + */ + +XK_F1 = 0xffbe, +XK_F2 = 0xffbf, +XK_F3 = 0xffc0, +XK_F4 = 0xffc1, +XK_F5 = 0xffc2, +XK_F6 = 0xffc3, +XK_F7 = 0xffc4, +XK_F8 = 0xffc5, +XK_F9 = 0xffc6, +XK_F10 = 0xffc7, +XK_F11 = 0xffc8, +XK_L1 = 0xffc8, +XK_F12 = 0xffc9, +XK_L2 = 0xffc9, +XK_F13 = 0xffca, +XK_L3 = 0xffca, +XK_F14 = 0xffcb, +XK_L4 = 0xffcb, +XK_F15 = 0xffcc, +XK_L5 = 0xffcc, +XK_F16 = 0xffcd, +XK_L6 = 0xffcd, +XK_F17 = 0xffce, +XK_L7 = 0xffce, +XK_F18 = 0xffcf, +XK_L8 = 0xffcf, +XK_F19 = 0xffd0, +XK_L9 = 0xffd0, +XK_F20 = 0xffd1, +XK_L10 = 0xffd1, +XK_F21 = 0xffd2, +XK_R1 = 0xffd2, +XK_F22 = 0xffd3, +XK_R2 = 0xffd3, +XK_F23 = 0xffd4, +XK_R3 = 0xffd4, +XK_F24 = 0xffd5, +XK_R4 = 0xffd5, +XK_F25 = 0xffd6, +XK_R5 = 0xffd6, +XK_F26 = 0xffd7, +XK_R6 = 0xffd7, +XK_F27 = 0xffd8, +XK_R7 = 0xffd8, +XK_F28 = 0xffd9, +XK_R8 = 0xffd9, +XK_F29 = 0xffda, +XK_R9 = 0xffda, +XK_F30 = 0xffdb, +XK_R10 = 0xffdb, +XK_F31 = 0xffdc, +XK_R11 = 0xffdc, +XK_F32 = 0xffdd, +XK_R12 = 0xffdd, +XK_F33 = 0xffde, +XK_R13 = 0xffde, +XK_F34 = 0xffdf, +XK_R14 = 0xffdf, +XK_F35 = 0xffe0, +XK_R15 = 0xffe0, + +/* Modifiers */ + +XK_Shift_L = 0xffe1, /* Left shift */ +XK_Shift_R = 0xffe2, /* Right shift */ +XK_Control_L = 0xffe3, /* Left control */ +XK_Control_R = 0xffe4, /* Right control */ +XK_Caps_Lock = 0xffe5, /* Caps lock */ +XK_Shift_Lock = 0xffe6, /* Shift lock */ + +XK_Meta_L = 0xffe7, /* Left meta */ +XK_Meta_R = 0xffe8, /* Right meta */ +XK_Alt_L = 0xffe9, /* Left alt */ +XK_Alt_R = 0xffea, /* Right alt */ +XK_Super_L = 0xffeb, /* Left super */ +XK_Super_R = 0xffec, /* Right super */ +XK_Hyper_L = 0xffed, /* Left hyper */ +XK_Hyper_R = 0xffee, /* Right hyper */ + +XK_ISO_Level3_Shift = 0xfe03, /* AltGr */ + +/* + * Latin 1 + * (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) + * Byte 3 = 0 + */ + +XK_space = 0x0020, /* U+0020 SPACE */ +XK_exclam = 0x0021, /* U+0021 EXCLAMATION MARK */ +XK_quotedbl = 0x0022, /* U+0022 QUOTATION MARK */ +XK_numbersign = 0x0023, /* U+0023 NUMBER SIGN */ +XK_dollar = 0x0024, /* U+0024 DOLLAR SIGN */ +XK_percent = 0x0025, /* U+0025 PERCENT SIGN */ +XK_ampersand = 0x0026, /* U+0026 AMPERSAND */ +XK_apostrophe = 0x0027, /* U+0027 APOSTROPHE */ +XK_quoteright = 0x0027, /* deprecated */ +XK_parenleft = 0x0028, /* U+0028 LEFT PARENTHESIS */ +XK_parenright = 0x0029, /* U+0029 RIGHT PARENTHESIS */ +XK_asterisk = 0x002a, /* U+002A ASTERISK */ +XK_plus = 0x002b, /* U+002B PLUS SIGN */ +XK_comma = 0x002c, /* U+002C COMMA */ +XK_minus = 0x002d, /* U+002D HYPHEN-MINUS */ +XK_period = 0x002e, /* U+002E FULL STOP */ +XK_slash = 0x002f, /* U+002F SOLIDUS */ +XK_0 = 0x0030, /* U+0030 DIGIT ZERO */ +XK_1 = 0x0031, /* U+0031 DIGIT ONE */ +XK_2 = 0x0032, /* U+0032 DIGIT TWO */ +XK_3 = 0x0033, /* U+0033 DIGIT THREE */ +XK_4 = 0x0034, /* U+0034 DIGIT FOUR */ +XK_5 = 0x0035, /* U+0035 DIGIT FIVE */ +XK_6 = 0x0036, /* U+0036 DIGIT SIX */ +XK_7 = 0x0037, /* U+0037 DIGIT SEVEN */ +XK_8 = 0x0038, /* U+0038 DIGIT EIGHT */ +XK_9 = 0x0039, /* U+0039 DIGIT NINE */ +XK_colon = 0x003a, /* U+003A COLON */ +XK_semicolon = 0x003b, /* U+003B SEMICOLON */ +XK_less = 0x003c, /* U+003C LESS-THAN SIGN */ +XK_equal = 0x003d, /* U+003D EQUALS SIGN */ +XK_greater = 0x003e, /* U+003E GREATER-THAN SIGN */ +XK_question = 0x003f, /* U+003F QUESTION MARK */ +XK_at = 0x0040, /* U+0040 COMMERCIAL AT */ +XK_A = 0x0041, /* U+0041 LATIN CAPITAL LETTER A */ +XK_B = 0x0042, /* U+0042 LATIN CAPITAL LETTER B */ +XK_C = 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ +XK_D = 0x0044, /* U+0044 LATIN CAPITAL LETTER D */ +XK_E = 0x0045, /* U+0045 LATIN CAPITAL LETTER E */ +XK_F = 0x0046, /* U+0046 LATIN CAPITAL LETTER F */ +XK_G = 0x0047, /* U+0047 LATIN CAPITAL LETTER G */ +XK_H = 0x0048, /* U+0048 LATIN CAPITAL LETTER H */ +XK_I = 0x0049, /* U+0049 LATIN CAPITAL LETTER I */ +XK_J = 0x004a, /* U+004A LATIN CAPITAL LETTER J */ +XK_K = 0x004b, /* U+004B LATIN CAPITAL LETTER K */ +XK_L = 0x004c, /* U+004C LATIN CAPITAL LETTER L */ +XK_M = 0x004d, /* U+004D LATIN CAPITAL LETTER M */ +XK_N = 0x004e, /* U+004E LATIN CAPITAL LETTER N */ +XK_O = 0x004f, /* U+004F LATIN CAPITAL LETTER O */ +XK_P = 0x0050, /* U+0050 LATIN CAPITAL LETTER P */ +XK_Q = 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */ +XK_R = 0x0052, /* U+0052 LATIN CAPITAL LETTER R */ +XK_S = 0x0053, /* U+0053 LATIN CAPITAL LETTER S */ +XK_T = 0x0054, /* U+0054 LATIN CAPITAL LETTER T */ +XK_U = 0x0055, /* U+0055 LATIN CAPITAL LETTER U */ +XK_V = 0x0056, /* U+0056 LATIN CAPITAL LETTER V */ +XK_W = 0x0057, /* U+0057 LATIN CAPITAL LETTER W */ +XK_X = 0x0058, /* U+0058 LATIN CAPITAL LETTER X */ +XK_Y = 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */ +XK_Z = 0x005a, /* U+005A LATIN CAPITAL LETTER Z */ +XK_bracketleft = 0x005b, /* U+005B LEFT SQUARE BRACKET */ +XK_backslash = 0x005c, /* U+005C REVERSE SOLIDUS */ +XK_bracketright = 0x005d, /* U+005D RIGHT SQUARE BRACKET */ +XK_asciicircum = 0x005e, /* U+005E CIRCUMFLEX ACCENT */ +XK_underscore = 0x005f, /* U+005F LOW LINE */ +XK_grave = 0x0060, /* U+0060 GRAVE ACCENT */ +XK_quoteleft = 0x0060, /* deprecated */ +XK_a = 0x0061, /* U+0061 LATIN SMALL LETTER A */ +XK_b = 0x0062, /* U+0062 LATIN SMALL LETTER B */ +XK_c = 0x0063, /* U+0063 LATIN SMALL LETTER C */ +XK_d = 0x0064, /* U+0064 LATIN SMALL LETTER D */ +XK_e = 0x0065, /* U+0065 LATIN SMALL LETTER E */ +XK_f = 0x0066, /* U+0066 LATIN SMALL LETTER F */ +XK_g = 0x0067, /* U+0067 LATIN SMALL LETTER G */ +XK_h = 0x0068, /* U+0068 LATIN SMALL LETTER H */ +XK_i = 0x0069, /* U+0069 LATIN SMALL LETTER I */ +XK_j = 0x006a, /* U+006A LATIN SMALL LETTER J */ +XK_k = 0x006b, /* U+006B LATIN SMALL LETTER K */ +XK_l = 0x006c, /* U+006C LATIN SMALL LETTER L */ +XK_m = 0x006d, /* U+006D LATIN SMALL LETTER M */ +XK_n = 0x006e, /* U+006E LATIN SMALL LETTER N */ +XK_o = 0x006f, /* U+006F LATIN SMALL LETTER O */ +XK_p = 0x0070, /* U+0070 LATIN SMALL LETTER P */ +XK_q = 0x0071, /* U+0071 LATIN SMALL LETTER Q */ +XK_r = 0x0072, /* U+0072 LATIN SMALL LETTER R */ +XK_s = 0x0073, /* U+0073 LATIN SMALL LETTER S */ +XK_t = 0x0074, /* U+0074 LATIN SMALL LETTER T */ +XK_u = 0x0075, /* U+0075 LATIN SMALL LETTER U */ +XK_v = 0x0076, /* U+0076 LATIN SMALL LETTER V */ +XK_w = 0x0077, /* U+0077 LATIN SMALL LETTER W */ +XK_x = 0x0078, /* U+0078 LATIN SMALL LETTER X */ +XK_y = 0x0079, /* U+0079 LATIN SMALL LETTER Y */ +XK_z = 0x007a, /* U+007A LATIN SMALL LETTER Z */ +XK_braceleft = 0x007b, /* U+007B LEFT CURLY BRACKET */ +XK_bar = 0x007c, /* U+007C VERTICAL LINE */ +XK_braceright = 0x007d, /* U+007D RIGHT CURLY BRACKET */ +XK_asciitilde = 0x007e, /* U+007E TILDE */ + +XK_nobreakspace = 0x00a0, /* U+00A0 NO-BREAK SPACE */ +XK_exclamdown = 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */ +XK_cent = 0x00a2, /* U+00A2 CENT SIGN */ +XK_sterling = 0x00a3, /* U+00A3 POUND SIGN */ +XK_currency = 0x00a4, /* U+00A4 CURRENCY SIGN */ +XK_yen = 0x00a5, /* U+00A5 YEN SIGN */ +XK_brokenbar = 0x00a6, /* U+00A6 BROKEN BAR */ +XK_section = 0x00a7, /* U+00A7 SECTION SIGN */ +XK_diaeresis = 0x00a8, /* U+00A8 DIAERESIS */ +XK_copyright = 0x00a9, /* U+00A9 COPYRIGHT SIGN */ +XK_ordfeminine = 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */ +XK_guillemotleft = 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_notsign = 0x00ac, /* U+00AC NOT SIGN */ +XK_hyphen = 0x00ad, /* U+00AD SOFT HYPHEN */ +XK_registered = 0x00ae, /* U+00AE REGISTERED SIGN */ +XK_macron = 0x00af, /* U+00AF MACRON */ +XK_degree = 0x00b0, /* U+00B0 DEGREE SIGN */ +XK_plusminus = 0x00b1, /* U+00B1 PLUS-MINUS SIGN */ +XK_twosuperior = 0x00b2, /* U+00B2 SUPERSCRIPT TWO */ +XK_threesuperior = 0x00b3, /* U+00B3 SUPERSCRIPT THREE */ +XK_acute = 0x00b4, /* U+00B4 ACUTE ACCENT */ +XK_mu = 0x00b5, /* U+00B5 MICRO SIGN */ +XK_paragraph = 0x00b6, /* U+00B6 PILCROW SIGN */ +XK_periodcentered = 0x00b7, /* U+00B7 MIDDLE DOT */ +XK_cedilla = 0x00b8, /* U+00B8 CEDILLA */ +XK_onesuperior = 0x00b9, /* U+00B9 SUPERSCRIPT ONE */ +XK_masculine = 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */ +XK_guillemotright = 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */ +XK_onequarter = 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */ +XK_onehalf = 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */ +XK_threequarters = 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */ +XK_questiondown = 0x00bf, /* U+00BF INVERTED QUESTION MARK */ +XK_Agrave = 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */ +XK_Aacute = 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */ +XK_Acircumflex = 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ +XK_Atilde = 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */ +XK_Adiaeresis = 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */ +XK_Aring = 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */ +XK_AE = 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */ +XK_Ccedilla = 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */ +XK_Egrave = 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */ +XK_Eacute = 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */ +XK_Ecircumflex = 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ +XK_Ediaeresis = 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */ +XK_Igrave = 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */ +XK_Iacute = 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */ +XK_Icircumflex = 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ +XK_Idiaeresis = 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */ +XK_ETH = 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */ +XK_Eth = 0x00d0, /* deprecated */ +XK_Ntilde = 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */ +XK_Ograve = 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */ +XK_Oacute = 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */ +XK_Ocircumflex = 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ +XK_Otilde = 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */ +XK_Odiaeresis = 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */ +XK_multiply = 0x00d7, /* U+00D7 MULTIPLICATION SIGN */ +XK_Oslash = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ooblique = 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ +XK_Ugrave = 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */ +XK_Uacute = 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */ +XK_Ucircumflex = 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ +XK_Udiaeresis = 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */ +XK_Yacute = 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */ +XK_THORN = 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */ +XK_Thorn = 0x00de, /* deprecated */ +XK_ssharp = 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */ +XK_agrave = 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */ +XK_aacute = 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */ +XK_acircumflex = 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */ +XK_atilde = 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */ +XK_adiaeresis = 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */ +XK_aring = 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */ +XK_ae = 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */ +XK_ccedilla = 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */ +XK_egrave = 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */ +XK_eacute = 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */ +XK_ecircumflex = 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */ +XK_ediaeresis = 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */ +XK_igrave = 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */ +XK_iacute = 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */ +XK_icircumflex = 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */ +XK_idiaeresis = 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */ +XK_eth = 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */ +XK_ntilde = 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */ +XK_ograve = 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */ +XK_oacute = 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */ +XK_ocircumflex = 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */ +XK_otilde = 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */ +XK_odiaeresis = 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */ +XK_division = 0x00f7, /* U+00F7 DIVISION SIGN */ +XK_oslash = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ooblique = 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ +XK_ugrave = 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */ +XK_uacute = 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */ +XK_ucircumflex = 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */ +XK_udiaeresis = 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */ +XK_yacute = 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ +XK_thorn = 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ +XK_ydiaeresis = 0x00ff; /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ diff --git a/public/novnc/include/keysymdef.js b/public/novnc/include/keysymdef.js new file mode 100644 index 00000000..f94445cf --- /dev/null +++ b/public/novnc/include/keysymdef.js @@ -0,0 +1,15 @@ +// This file describes mappings from Unicode codepoints to the keysym values +// (and optionally, key names) expected by the RFB protocol +// How this file was generated: +// node /Users/jalf/dev/mi/novnc/utils/parse.js /opt/X11/include/X11/keysymdef.h +var keysyms = (function(){ + "use strict"; + var keynames = null; + var codepoints = {"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"49":49,"50":50,"51":51,"52":52,"53":53,"54":54,"55":55,"56":56,"57":57,"58":58,"59":59,"60":60,"61":61,"62":62,"63":63,"64":64,"65":65,"66":66,"67":67,"68":68,"69":69,"70":70,"71":71,"72":72,"73":73,"74":74,"75":75,"76":76,"77":77,"78":78,"79":79,"80":80,"81":81,"82":82,"83":83,"84":84,"85":85,"86":86,"87":87,"88":88,"89":89,"90":90,"91":91,"92":92,"93":93,"94":94,"95":95,"96":96,"97":97,"98":98,"99":99,"100":100,"101":101,"102":102,"103":103,"104":104,"105":105,"106":106,"107":107,"108":108,"109":109,"110":110,"111":111,"112":112,"113":113,"114":114,"115":115,"116":116,"117":117,"118":118,"119":119,"120":120,"121":121,"122":122,"123":123,"124":124,"125":125,"126":126,"160":160,"161":161,"162":162,"163":163,"164":164,"165":165,"166":166,"167":167,"168":168,"169":169,"170":170,"171":171,"172":172,"173":173,"174":174,"175":175,"176":176,"177":177,"178":178,"179":179,"180":180,"181":181,"182":182,"183":183,"184":184,"185":185,"186":186,"187":187,"188":188,"189":189,"190":190,"191":191,"192":192,"193":193,"194":194,"195":195,"196":196,"197":197,"198":198,"199":199,"200":200,"201":201,"202":202,"203":203,"204":204,"205":205,"206":206,"207":207,"208":208,"209":209,"210":210,"211":211,"212":212,"213":213,"214":214,"215":215,"216":216,"217":217,"218":218,"219":219,"220":220,"221":221,"222":222,"223":223,"224":224,"225":225,"226":226,"227":227,"228":228,"229":229,"230":230,"231":231,"232":232,"233":233,"234":234,"235":235,"236":236,"237":237,"238":238,"239":239,"240":240,"241":241,"242":242,"243":243,"244":244,"245":245,"246":246,"247":247,"248":248,"249":249,"250":250,"251":251,"252":252,"253":253,"254":254,"255":255,"256":960,"257":992,"258":451,"259":483,"260":417,"261":433,"262":454,"263":486,"264":710,"265":742,"266":709,"267":741,"268":456,"269":488,"270":463,"271":495,"272":464,"273":496,"274":938,"275":954,"278":972,"279":1004,"280":458,"281":490,"282":460,"283":492,"284":728,"285":760,"286":683,"287":699,"288":725,"289":757,"290":939,"291":955,"292":678,"293":694,"294":673,"295":689,"296":933,"297":949,"298":975,"299":1007,"300":16777516,"301":16777517,"302":967,"303":999,"304":681,"305":697,"308":684,"309":700,"310":979,"311":1011,"312":930,"313":453,"314":485,"315":934,"316":950,"317":421,"318":437,"321":419,"322":435,"323":465,"324":497,"325":977,"326":1009,"327":466,"328":498,"330":957,"331":959,"332":978,"333":1010,"336":469,"337":501,"338":5052,"339":5053,"340":448,"341":480,"342":931,"343":947,"344":472,"345":504,"346":422,"347":438,"348":734,"349":766,"350":426,"351":442,"352":425,"353":441,"354":478,"355":510,"356":427,"357":443,"358":940,"359":956,"360":989,"361":1021,"362":990,"363":1022,"364":733,"365":765,"366":473,"367":505,"368":475,"369":507,"370":985,"371":1017,"372":16777588,"373":16777589,"374":16777590,"375":16777591,"376":5054,"377":428,"378":444,"379":431,"380":447,"381":430,"382":446,"399":16777615,"402":2294,"415":16777631,"416":16777632,"417":16777633,"431":16777647,"432":16777648,"437":16777653,"438":16777654,"439":16777655,"466":16777681,"486":16777702,"487":16777703,"601":16777817,"629":16777845,"658":16777874,"711":439,"728":418,"729":511,"731":434,"733":445,"901":1966,"902":1953,"904":1954,"905":1955,"906":1956,"908":1959,"910":1960,"911":1963,"912":1974,"913":1985,"914":1986,"915":1987,"916":1988,"917":1989,"918":1990,"919":1991,"920":1992,"921":1993,"922":1994,"923":1995,"924":1996,"925":1997,"926":1998,"927":1999,"928":2000,"929":2001,"931":2002,"932":2004,"933":2005,"934":2006,"935":2007,"936":2008,"937":2009,"938":1957,"939":1961,"940":1969,"941":1970,"942":1971,"943":1972,"944":1978,"945":2017,"946":2018,"947":2019,"948":2020,"949":2021,"950":2022,"951":2023,"952":2024,"953":2025,"954":2026,"955":2027,"956":2028,"957":2029,"958":2030,"959":2031,"960":2032,"961":2033,"962":2035,"963":2034,"964":2036,"965":2037,"966":2038,"967":2039,"968":2040,"969":2041,"970":1973,"971":1977,"972":1975,"973":1976,"974":1979,"1025":1715,"1026":1713,"1027":1714,"1028":1716,"1029":1717,"1030":1718,"1031":1719,"1032":1720,"1033":1721,"1034":1722,"1035":1723,"1036":1724,"1038":1726,"1039":1727,"1040":1761,"1041":1762,"1042":1783,"1043":1767,"1044":1764,"1045":1765,"1046":1782,"1047":1786,"1048":1769,"1049":1770,"1050":1771,"1051":1772,"1052":1773,"1053":1774,"1054":1775,"1055":1776,"1056":1778,"1057":1779,"1058":1780,"1059":1781,"1060":1766,"1061":1768,"1062":1763,"1063":1790,"1064":1787,"1065":1789,"1066":1791,"1067":1785,"1068":1784,"1069":1788,"1070":1760,"1071":1777,"1072":1729,"1073":1730,"1074":1751,"1075":1735,"1076":1732,"1077":1733,"1078":1750,"1079":1754,"1080":1737,"1081":1738,"1082":1739,"1083":1740,"1084":1741,"1085":1742,"1086":1743,"1087":1744,"1088":1746,"1089":1747,"1090":1748,"1091":1749,"1092":1734,"1093":1736,"1094":1731,"1095":1758,"1096":1755,"1097":1757,"1098":1759,"1099":1753,"1100":1752,"1101":1756,"1102":1728,"1103":1745,"1105":1699,"1106":1697,"1107":1698,"1108":1700,"1109":1701,"1110":1702,"1111":1703,"1112":1704,"1113":1705,"1114":1706,"1115":1707,"1116":1708,"1118":1710,"1119":1711,"1168":1725,"1169":1709,"1170":16778386,"1171":16778387,"1174":16778390,"1175":16778391,"1178":16778394,"1179":16778395,"1180":16778396,"1181":16778397,"1186":16778402,"1187":16778403,"1198":16778414,"1199":16778415,"1200":16778416,"1201":16778417,"1202":16778418,"1203":16778419,"1206":16778422,"1207":16778423,"1208":16778424,"1209":16778425,"1210":16778426,"1211":16778427,"1240":16778456,"1241":16778457,"1250":16778466,"1251":16778467,"1256":16778472,"1257":16778473,"1262":16778478,"1263":16778479,"1329":16778545,"1330":16778546,"1331":16778547,"1332":16778548,"1333":16778549,"1334":16778550,"1335":16778551,"1336":16778552,"1337":16778553,"1338":16778554,"1339":16778555,"1340":16778556,"1341":16778557,"1342":16778558,"1343":16778559,"1344":16778560,"1345":16778561,"1346":16778562,"1347":16778563,"1348":16778564,"1349":16778565,"1350":16778566,"1351":16778567,"1352":16778568,"1353":16778569,"1354":16778570,"1355":16778571,"1356":16778572,"1357":16778573,"1358":16778574,"1359":16778575,"1360":16778576,"1361":16778577,"1362":16778578,"1363":16778579,"1364":16778580,"1365":16778581,"1366":16778582,"1370":16778586,"1371":16778587,"1372":16778588,"1373":16778589,"1374":16778590,"1377":16778593,"1378":16778594,"1379":16778595,"1380":16778596,"1381":16778597,"1382":16778598,"1383":16778599,"1384":16778600,"1385":16778601,"1386":16778602,"1387":16778603,"1388":16778604,"1389":16778605,"1390":16778606,"1391":16778607,"1392":16778608,"1393":16778609,"1394":16778610,"1395":16778611,"1396":16778612,"1397":16778613,"1398":16778614,"1399":16778615,"1400":16778616,"1401":16778617,"1402":16778618,"1403":16778619,"1404":16778620,"1405":16778621,"1406":16778622,"1407":16778623,"1408":16778624,"1409":16778625,"1410":16778626,"1411":16778627,"1412":16778628,"1413":16778629,"1414":16778630,"1415":16778631,"1417":16778633,"1418":16778634,"1488":3296,"1489":3297,"1490":3298,"1491":3299,"1492":3300,"1493":3301,"1494":3302,"1495":3303,"1496":3304,"1497":3305,"1498":3306,"1499":3307,"1500":3308,"1501":3309,"1502":3310,"1503":3311,"1504":3312,"1505":3313,"1506":3314,"1507":3315,"1508":3316,"1509":3317,"1510":3318,"1511":3319,"1512":3320,"1513":3321,"1514":3322,"1548":1452,"1563":1467,"1567":1471,"1569":1473,"1570":1474,"1571":1475,"1572":1476,"1573":1477,"1574":1478,"1575":1479,"1576":1480,"1577":1481,"1578":1482,"1579":1483,"1580":1484,"1581":1485,"1582":1486,"1583":1487,"1584":1488,"1585":1489,"1586":1490,"1587":1491,"1588":1492,"1589":1493,"1590":1494,"1591":1495,"1592":1496,"1593":1497,"1594":1498,"1600":1504,"1601":1505,"1602":1506,"1603":1507,"1604":1508,"1605":1509,"1606":1510,"1607":1511,"1608":1512,"1609":1513,"1610":1514,"1611":1515,"1612":1516,"1613":1517,"1614":1518,"1615":1519,"1616":1520,"1617":1521,"1618":1522,"1619":16778835,"1620":16778836,"1621":16778837,"1632":16778848,"1633":16778849,"1634":16778850,"1635":16778851,"1636":16778852,"1637":16778853,"1638":16778854,"1639":16778855,"1640":16778856,"1641":16778857,"1642":16778858,"1648":16778864,"1657":16778873,"1662":16778878,"1670":16778886,"1672":16778888,"1681":16778897,"1688":16778904,"1700":16778916,"1705":16778921,"1711":16778927,"1722":16778938,"1726":16778942,"1729":16778945,"1740":16778956,"1746":16778962,"1748":16778964,"1776":16778992,"1777":16778993,"1778":16778994,"1779":16778995,"1780":16778996,"1781":16778997,"1782":16778998,"1783":16778999,"1784":16779000,"1785":16779001,"3458":16780674,"3459":16780675,"3461":16780677,"3462":16780678,"3463":16780679,"3464":16780680,"3465":16780681,"3466":16780682,"3467":16780683,"3468":16780684,"3469":16780685,"3470":16780686,"3471":16780687,"3472":16780688,"3473":16780689,"3474":16780690,"3475":16780691,"3476":16780692,"3477":16780693,"3478":16780694,"3482":16780698,"3483":16780699,"3484":16780700,"3485":16780701,"3486":16780702,"3487":16780703,"3488":16780704,"3489":16780705,"3490":16780706,"3491":16780707,"3492":16780708,"3493":16780709,"3494":16780710,"3495":16780711,"3496":16780712,"3497":16780713,"3498":16780714,"3499":16780715,"3500":16780716,"3501":16780717,"3502":16780718,"3503":16780719,"3504":16780720,"3505":16780721,"3507":16780723,"3508":16780724,"3509":16780725,"3510":16780726,"3511":16780727,"3512":16780728,"3513":16780729,"3514":16780730,"3515":16780731,"3517":16780733,"3520":16780736,"3521":16780737,"3522":16780738,"3523":16780739,"3524":16780740,"3525":16780741,"3526":16780742,"3530":16780746,"3535":16780751,"3536":16780752,"3537":16780753,"3538":16780754,"3539":16780755,"3540":16780756,"3542":16780758,"3544":16780760,"3545":16780761,"3546":16780762,"3547":16780763,"3548":16780764,"3549":16780765,"3550":16780766,"3551":16780767,"3570":16780786,"3571":16780787,"3572":16780788,"3585":3489,"3586":3490,"3587":3491,"3588":3492,"3589":3493,"3590":3494,"3591":3495,"3592":3496,"3593":3497,"3594":3498,"3595":3499,"3596":3500,"3597":3501,"3598":3502,"3599":3503,"3600":3504,"3601":3505,"3602":3506,"3603":3507,"3604":3508,"3605":3509,"3606":3510,"3607":3511,"3608":3512,"3609":3513,"3610":3514,"3611":3515,"3612":3516,"3613":3517,"3614":3518,"3615":3519,"3616":3520,"3617":3521,"3618":3522,"3619":3523,"3620":3524,"3621":3525,"3622":3526,"3623":3527,"3624":3528,"3625":3529,"3626":3530,"3627":3531,"3628":3532,"3629":3533,"3630":3534,"3631":3535,"3632":3536,"3633":3537,"3634":3538,"3635":3539,"3636":3540,"3637":3541,"3638":3542,"3639":3543,"3640":3544,"3641":3545,"3642":3546,"3647":3551,"3648":3552,"3649":3553,"3650":3554,"3651":3555,"3652":3556,"3653":3557,"3654":3558,"3655":3559,"3656":3560,"3657":3561,"3658":3562,"3659":3563,"3660":3564,"3661":3565,"3664":3568,"3665":3569,"3666":3570,"3667":3571,"3668":3572,"3669":3573,"3670":3574,"3671":3575,"3672":3576,"3673":3577,"4304":16781520,"4305":16781521,"4306":16781522,"4307":16781523,"4308":16781524,"4309":16781525,"4310":16781526,"4311":16781527,"4312":16781528,"4313":16781529,"4314":16781530,"4315":16781531,"4316":16781532,"4317":16781533,"4318":16781534,"4319":16781535,"4320":16781536,"4321":16781537,"4322":16781538,"4323":16781539,"4324":16781540,"4325":16781541,"4326":16781542,"4327":16781543,"4328":16781544,"4329":16781545,"4330":16781546,"4331":16781547,"4332":16781548,"4333":16781549,"4334":16781550,"4335":16781551,"4336":16781552,"4337":16781553,"4338":16781554,"4339":16781555,"4340":16781556,"4341":16781557,"4342":16781558,"7682":16784898,"7683":16784899,"7690":16784906,"7691":16784907,"7710":16784926,"7711":16784927,"7734":16784950,"7735":16784951,"7744":16784960,"7745":16784961,"7766":16784982,"7767":16784983,"7776":16784992,"7777":16784993,"7786":16785002,"7787":16785003,"7808":16785024,"7809":16785025,"7810":16785026,"7811":16785027,"7812":16785028,"7813":16785029,"7818":16785034,"7819":16785035,"7840":16785056,"7841":16785057,"7842":16785058,"7843":16785059,"7844":16785060,"7845":16785061,"7846":16785062,"7847":16785063,"7848":16785064,"7849":16785065,"7850":16785066,"7851":16785067,"7852":16785068,"7853":16785069,"7854":16785070,"7855":16785071,"7856":16785072,"7857":16785073,"7858":16785074,"7859":16785075,"7860":16785076,"7861":16785077,"7862":16785078,"7863":16785079,"7864":16785080,"7865":16785081,"7866":16785082,"7867":16785083,"7868":16785084,"7869":16785085,"7870":16785086,"7871":16785087,"7872":16785088,"7873":16785089,"7874":16785090,"7875":16785091,"7876":16785092,"7877":16785093,"7878":16785094,"7879":16785095,"7880":16785096,"7881":16785097,"7882":16785098,"7883":16785099,"7884":16785100,"7885":16785101,"7886":16785102,"7887":16785103,"7888":16785104,"7889":16785105,"7890":16785106,"7891":16785107,"7892":16785108,"7893":16785109,"7894":16785110,"7895":16785111,"7896":16785112,"7897":16785113,"7898":16785114,"7899":16785115,"7900":16785116,"7901":16785117,"7902":16785118,"7903":16785119,"7904":16785120,"7905":16785121,"7906":16785122,"7907":16785123,"7908":16785124,"7909":16785125,"7910":16785126,"7911":16785127,"7912":16785128,"7913":16785129,"7914":16785130,"7915":16785131,"7916":16785132,"7917":16785133,"7918":16785134,"7919":16785135,"7920":16785136,"7921":16785137,"7922":16785138,"7923":16785139,"7924":16785140,"7925":16785141,"7926":16785142,"7927":16785143,"7928":16785144,"7929":16785145,"8194":2722,"8195":2721,"8196":2723,"8197":2724,"8199":2725,"8200":2726,"8201":2727,"8202":2728,"8210":2747,"8211":2730,"8212":2729,"8213":1967,"8215":3295,"8216":2768,"8217":2769,"8218":2813,"8220":2770,"8221":2771,"8222":2814,"8224":2801,"8225":2802,"8226":2790,"8229":2735,"8230":2734,"8240":2773,"8242":2774,"8243":2775,"8248":2812,"8254":1150,"8304":16785520,"8308":16785524,"8309":16785525,"8310":16785526,"8311":16785527,"8312":16785528,"8313":16785529,"8320":16785536,"8321":16785537,"8322":16785538,"8323":16785539,"8324":16785540,"8325":16785541,"8326":16785542,"8327":16785543,"8328":16785544,"8329":16785545,"8352":16785568,"8353":16785569,"8354":16785570,"8355":16785571,"8356":16785572,"8357":16785573,"8358":16785574,"8359":16785575,"8360":16785576,"8361":3839,"8362":16785578,"8363":16785579,"8364":8364,"8453":2744,"8470":1712,"8471":2811,"8478":2772,"8482":2761,"8531":2736,"8532":2737,"8533":2738,"8534":2739,"8535":2740,"8536":2741,"8537":2742,"8538":2743,"8539":2755,"8540":2756,"8541":2757,"8542":2758,"8592":2299,"8593":2300,"8594":2301,"8595":2302,"8658":2254,"8660":2253,"8706":2287,"8709":16785925,"8711":2245,"8712":16785928,"8713":16785929,"8715":16785931,"8728":3018,"8730":2262,"8731":16785947,"8732":16785948,"8733":2241,"8734":2242,"8743":2270,"8744":2271,"8745":2268,"8746":2269,"8747":2239,"8748":16785964,"8749":16785965,"8756":2240,"8757":16785973,"8764":2248,"8771":2249,"8773":16785992,"8775":16785991,"8800":2237,"8801":2255,"8802":16786018,"8803":16786019,"8804":2236,"8805":2238,"8834":2266,"8835":2267,"8866":3068,"8867":3036,"8868":3010,"8869":3022,"8968":3027,"8970":3012,"8981":2810,"8992":2212,"8993":2213,"9109":3020,"9115":2219,"9117":2220,"9118":2221,"9120":2222,"9121":2215,"9123":2216,"9124":2217,"9126":2218,"9128":2223,"9132":2224,"9143":2209,"9146":2543,"9147":2544,"9148":2546,"9149":2547,"9225":2530,"9226":2533,"9227":2537,"9228":2531,"9229":2532,"9251":2732,"9252":2536,"9472":2211,"9474":2214,"9484":2210,"9488":2539,"9492":2541,"9496":2538,"9500":2548,"9508":2549,"9516":2551,"9524":2550,"9532":2542,"9618":2529,"9642":2791,"9643":2785,"9644":2779,"9645":2786,"9646":2783,"9647":2767,"9650":2792,"9651":2787,"9654":2781,"9655":2765,"9660":2793,"9661":2788,"9664":2780,"9665":2764,"9670":2528,"9675":2766,"9679":2782,"9702":2784,"9734":2789,"9742":2809,"9747":2762,"9756":2794,"9758":2795,"9792":2808,"9794":2807,"9827":2796,"9829":2798,"9830":2797,"9837":2806,"9839":2805,"10003":2803,"10007":2804,"10013":2777,"10016":2800,"10216":2748,"10217":2750,"10240":16787456,"10241":16787457,"10242":16787458,"10243":16787459,"10244":16787460,"10245":16787461,"10246":16787462,"10247":16787463,"10248":16787464,"10249":16787465,"10250":16787466,"10251":16787467,"10252":16787468,"10253":16787469,"10254":16787470,"10255":16787471,"10256":16787472,"10257":16787473,"10258":16787474,"10259":16787475,"10260":16787476,"10261":16787477,"10262":16787478,"10263":16787479,"10264":16787480,"10265":16787481,"10266":16787482,"10267":16787483,"10268":16787484,"10269":16787485,"10270":16787486,"10271":16787487,"10272":16787488,"10273":16787489,"10274":16787490,"10275":16787491,"10276":16787492,"10277":16787493,"10278":16787494,"10279":16787495,"10280":16787496,"10281":16787497,"10282":16787498,"10283":16787499,"10284":16787500,"10285":16787501,"10286":16787502,"10287":16787503,"10288":16787504,"10289":16787505,"10290":16787506,"10291":16787507,"10292":16787508,"10293":16787509,"10294":16787510,"10295":16787511,"10296":16787512,"10297":16787513,"10298":16787514,"10299":16787515,"10300":16787516,"10301":16787517,"10302":16787518,"10303":16787519,"10304":16787520,"10305":16787521,"10306":16787522,"10307":16787523,"10308":16787524,"10309":16787525,"10310":16787526,"10311":16787527,"10312":16787528,"10313":16787529,"10314":16787530,"10315":16787531,"10316":16787532,"10317":16787533,"10318":16787534,"10319":16787535,"10320":16787536,"10321":16787537,"10322":16787538,"10323":16787539,"10324":16787540,"10325":16787541,"10326":16787542,"10327":16787543,"10328":16787544,"10329":16787545,"10330":16787546,"10331":16787547,"10332":16787548,"10333":16787549,"10334":16787550,"10335":16787551,"10336":16787552,"10337":16787553,"10338":16787554,"10339":16787555,"10340":16787556,"10341":16787557,"10342":16787558,"10343":16787559,"10344":16787560,"10345":16787561,"10346":16787562,"10347":16787563,"10348":16787564,"10349":16787565,"10350":16787566,"10351":16787567,"10352":16787568,"10353":16787569,"10354":16787570,"10355":16787571,"10356":16787572,"10357":16787573,"10358":16787574,"10359":16787575,"10360":16787576,"10361":16787577,"10362":16787578,"10363":16787579,"10364":16787580,"10365":16787581,"10366":16787582,"10367":16787583,"10368":16787584,"10369":16787585,"10370":16787586,"10371":16787587,"10372":16787588,"10373":16787589,"10374":16787590,"10375":16787591,"10376":16787592,"10377":16787593,"10378":16787594,"10379":16787595,"10380":16787596,"10381":16787597,"10382":16787598,"10383":16787599,"10384":16787600,"10385":16787601,"10386":16787602,"10387":16787603,"10388":16787604,"10389":16787605,"10390":16787606,"10391":16787607,"10392":16787608,"10393":16787609,"10394":16787610,"10395":16787611,"10396":16787612,"10397":16787613,"10398":16787614,"10399":16787615,"10400":16787616,"10401":16787617,"10402":16787618,"10403":16787619,"10404":16787620,"10405":16787621,"10406":16787622,"10407":16787623,"10408":16787624,"10409":16787625,"10410":16787626,"10411":16787627,"10412":16787628,"10413":16787629,"10414":16787630,"10415":16787631,"10416":16787632,"10417":16787633,"10418":16787634,"10419":16787635,"10420":16787636,"10421":16787637,"10422":16787638,"10423":16787639,"10424":16787640,"10425":16787641,"10426":16787642,"10427":16787643,"10428":16787644,"10429":16787645,"10430":16787646,"10431":16787647,"10432":16787648,"10433":16787649,"10434":16787650,"10435":16787651,"10436":16787652,"10437":16787653,"10438":16787654,"10439":16787655,"10440":16787656,"10441":16787657,"10442":16787658,"10443":16787659,"10444":16787660,"10445":16787661,"10446":16787662,"10447":16787663,"10448":16787664,"10449":16787665,"10450":16787666,"10451":16787667,"10452":16787668,"10453":16787669,"10454":16787670,"10455":16787671,"10456":16787672,"10457":16787673,"10458":16787674,"10459":16787675,"10460":16787676,"10461":16787677,"10462":16787678,"10463":16787679,"10464":16787680,"10465":16787681,"10466":16787682,"10467":16787683,"10468":16787684,"10469":16787685,"10470":16787686,"10471":16787687,"10472":16787688,"10473":16787689,"10474":16787690,"10475":16787691,"10476":16787692,"10477":16787693,"10478":16787694,"10479":16787695,"10480":16787696,"10481":16787697,"10482":16787698,"10483":16787699,"10484":16787700,"10485":16787701,"10486":16787702,"10487":16787703,"10488":16787704,"10489":16787705,"10490":16787706,"10491":16787707,"10492":16787708,"10493":16787709,"10494":16787710,"10495":16787711,"12289":1188,"12290":1185,"12300":1186,"12301":1187,"12443":1246,"12444":1247,"12449":1191,"12450":1201,"12451":1192,"12452":1202,"12453":1193,"12454":1203,"12455":1194,"12456":1204,"12457":1195,"12458":1205,"12459":1206,"12461":1207,"12463":1208,"12465":1209,"12467":1210,"12469":1211,"12471":1212,"12473":1213,"12475":1214,"12477":1215,"12479":1216,"12481":1217,"12483":1199,"12484":1218,"12486":1219,"12488":1220,"12490":1221,"12491":1222,"12492":1223,"12493":1224,"12494":1225,"12495":1226,"12498":1227,"12501":1228,"12504":1229,"12507":1230,"12510":1231,"12511":1232,"12512":1233,"12513":1234,"12514":1235,"12515":1196,"12516":1236,"12517":1197,"12518":1237,"12519":1198,"12520":1238,"12521":1239,"12522":1240,"12523":1241,"12524":1242,"12525":1243,"12527":1244,"12530":1190,"12531":1245,"12539":1189,"12540":1200}; + + function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; } + return { + fromUnicode : function(u) { return lookup(codepoints[u]); }, + lookup : lookup + }; +})(); diff --git a/public/novnc/include/logo.js b/public/novnc/include/logo.js new file mode 100644 index 00000000..befa598c --- /dev/null +++ b/public/novnc/include/logo.js @@ -0,0 +1 @@ +noVNC_logo = {"width": 640, "height": 435, "data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAGzCAYAAAC/y6a9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAStAAAErQBBHTWggAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7N13fBvlwQfw3522ZMm2vPdIGCFkA4GyoYyGsCmjk+7dQksHL2/H2/dtC4W2tLTlfelu2VA2lEILFCgQIHEGJCQkdjzkLdmWZGvfvX8oOkmJEy/pNO73/Xz44DtLzz2RT7qfnnXC8uXLZUxDlqfdnUYQhIP+bjbPn+5xhypzrmUf6rGzOc5cjzVduXN9/nTPyfRrMt/jzOcY05U5n3L2f95s/34LPW4m/p6FbLp/73xe+5nKnWuZs/07ZOOcnusx5nucbJU727LneuxslDmdTBxn/2NmusyEuZS7kHMxG/XP5Gf3TOVmQzY+u/PhPMnkMcSsH5WIiIiI8goDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHGMAASERERaQwDIBEREZHG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"}; diff --git a/public/novnc/include/playback.js b/public/novnc/include/playback.js new file mode 100644 index 00000000..203576f6 --- /dev/null +++ b/public/novnc/include/playback.js @@ -0,0 +1,120 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +"use strict"; +/*jslint browser: true, white: false */ +/*global Util, VNC_frame_data, finish */ + +var rfb, mode, test_state, frame_idx, frame_length, + iteration, iterations, istart_time, + + // Pre-declarations for jslint + send_array, next_iteration, queue_next_packet, do_packet, enable_test_mode; + +// Override send_array +send_array = function (arr) { + // Stub out send_array +}; + +enable_test_mode = function () { + rfb._sock._mode = VNC_frame_encoding; + rfb._sock.send = send_array; + rfb._sock.close = function () {}; + rfb._sock.flush = function () {}; + rfb._checkEvents = function () {}; + rfb.connect = function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; + this._sock.init('binary', 'ws'); + this._updateState('ProtocolVersion', "Starting VNC handshake"); + }; +}; + +next_iteration = function () { + rfb = new RFB({'target': $D('VNC_canvas'), + 'onUpdateState': updateState}); + enable_test_mode(); + + if (iteration === 0) { + frame_length = VNC_frame_data.length; + test_state = 'running'; + } + + if (test_state !== 'running') { return; } + + iteration += 1; + if (iteration > iterations) { + finish(); + return; + } + + frame_idx = 0; + istart_time = (new Date()).getTime(); + rfb.connect('test', 0, "bogus"); + + queue_next_packet(); + +}; + +queue_next_packet = function () { + var frame, foffset, toffset, delay; + if (test_state !== 'running') { return; } + + frame = VNC_frame_data[frame_idx]; + while ((frame_idx < frame_length) && (frame.charAt(0) === "}")) { + //Util.Debug("Send frame " + frame_idx); + frame_idx += 1; + frame = VNC_frame_data[frame_idx]; + } + + if (frame === 'EOF') { + Util.Debug("Finished, found EOF"); + next_iteration(); + return; + } + if (frame_idx >= frame_length) { + Util.Debug("Finished, no more frames"); + next_iteration(); + return; + } + + if (mode === 'realtime') { + foffset = frame.slice(1, frame.indexOf('{', 1)); + toffset = (new Date()).getTime() - istart_time; + delay = foffset - toffset; + if (delay < 1) { + delay = 1; + } + + setTimeout(do_packet, delay); + } else { + setTimeout(do_packet, 1); + } +}; + +var bytes_processed = 0; + +do_packet = function () { + //Util.Debug("Processing frame: " + frame_idx); + var frame = VNC_frame_data[frame_idx], + start = frame.indexOf('{', 1) + 1; + bytes_processed += frame.length - start; + if (VNC_frame_encoding === 'binary') { + var u8 = new Uint8Array(frame.length - start); + for (var i = 0; i < frame.length - start; i++) { + u8[i] = frame.charCodeAt(start + i); + } + rfb._sock._recv_message({'data' : u8}); + } else { + rfb._sock._recv_message({'data' : frame.slice(start)}); + } + frame_idx += 1; + + queue_next_packet(); +}; + diff --git a/public/novnc/include/rfb.js b/public/novnc/include/rfb.js new file mode 100644 index 00000000..d0e6c8f7 --- /dev/null +++ b/public/novnc/include/rfb.js @@ -0,0 +1,2148 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + * TIGHT decoder portion: + * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) + */ + +/*jslint white: false, browser: true */ +/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ + +var RFB; + +(function () { + "use strict"; + RFB = function (defaults) { + if (!defaults) { + defaults = {}; + } + + this._rfb_host = ''; + this._rfb_port = 5900; + this._rfb_password = ''; + this._rfb_path = ''; + + this._rfb_state = 'disconnected'; + this._rfb_version = 0; + this._rfb_max_version = 3.8; + this._rfb_auth_scheme = ''; + + this._rfb_tightvnc = false; + this._rfb_xvp_ver = 0; + + // In preference order + this._encodings = [ + ['COPYRECT', 0x01 ], + ['TIGHT', 0x07 ], + ['TIGHT_PNG', -260 ], + ['HEXTILE', 0x05 ], + ['RRE', 0x02 ], + ['RAW', 0x00 ], + ['DesktopSize', -223 ], + ['Cursor', -239 ], + + // Psuedo-encoding settings + //['JPEG_quality_lo', -32 ], + ['JPEG_quality_med', -26 ], + //['JPEG_quality_hi', -23 ], + //['compress_lo', -255 ], + ['compress_hi', -247 ], + ['last_rect', -224 ], + ['xvp', -309 ], + ['ExtendedDesktopSize', -308 ] + ]; + + this._encHandlers = {}; + this._encNames = {}; + this._encStats = {}; + + this._sock = null; // Websock object + this._display = null; // Display object + this._keyboard = null; // Keyboard input handler object + this._mouse = null; // Mouse input handler object + this._sendTimer = null; // Send Queue check timer + this._disconnTimer = null; // disconnection timer + this._msgTimer = null; // queued handle_msg timer + + // Frame buffer update state + this._FBU = { + rects: 0, + subrects: 0, // RRE + lines: 0, // RAW + tiles: 0, // HEXTILE + bytes: 0, + x: 0, + y: 0, + width: 0, + height: 0, + encoding: 0, + subencoding: -1, + background: null, + zlib: [] // TIGHT zlib streams + }; + + this._fb_Bpp = 4; + this._fb_depth = 3; + this._fb_width = 0; + this._fb_height = 0; + this._fb_name = ""; + + this._destBuff = null; + this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + + this._rre_chunk_sz = 100; + + this._timing = { + last_fbu: 0, + fbu_total: 0, + fbu_total_cnt: 0, + full_fbu_total: 0, + full_fbu_cnt: 0, + + fbu_rt_start: 0, + fbu_rt_total: 0, + fbu_rt_cnt: 0, + pixels: 0 + }; + + this._supportsSetDesktopSize = false; + this._screen_id = 0; + this._screen_flags = 0; + + // Mouse state + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._viewportDragging = false; + this._viewportDragPos = {}; + this._viewportHasMoved = false; + + // set the default value on user-facing properties + Util.set_defaults(this, defaults, { + 'target': 'null', // VNC display rendering Canvas object + 'focusContainer': document, // DOM element that captures keyboard input + 'encrypt': false, // Use TLS/SSL/wss encryption + 'true_color': true, // Request true color pixel data + 'local_cursor': false, // Request locally rendered cursor + 'shared': true, // Request shared mode + 'view_only': false, // Disable client mouse/keyboard + 'xvp_password_sep': '@', // Separator for XVP password fields + 'disconnectTimeout': 3, // Time (s) to wait for disconnection + 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection + 'repeaterID': '', // [UltraVNC] RepeaterID to connect to + 'viewportDrag': false, // Move the viewport on mouse drags + + // Callback functions + 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change + 'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required + 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received + 'onBell': function () { }, // onBell(rfb): RFB Bell message received + 'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed + 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized + 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received + 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection + }); + + // main setup + Util.Debug(">> RFB.constructor"); + + // populate encHandlers with bound versions + Object.keys(RFB.encodingHandlers).forEach(function (encName) { + this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this); + }.bind(this)); + + // Create lookup tables based on encoding number + for (var i = 0; i < this._encodings.length; i++) { + this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]]; + this._encNames[this._encodings[i][1]] = this._encodings[i][0]; + this._encStats[this._encodings[i][1]] = [0, 0]; + } + + // NB: nothing that needs explicit teardown should be done + // before this point, since this can throw an exception + try { + this._display = new Display({target: this._target}); + } catch (exc) { + Util.Error("Display exception: " + exc); + throw exc; + } + + this._keyboard = new Keyboard({target: this._focusContainer, + onKeyPress: this._handleKeyPress.bind(this)}); + + this._mouse = new Mouse({target: this._target, + onMouseButton: this._handleMouseButton.bind(this), + onMouseMove: this._handleMouseMove.bind(this), + notify: this._keyboard.sync.bind(this._keyboard)}); + + this._sock = new Websock(); + this._sock.on('message', this._handle_message.bind(this)); + this._sock.on('open', function () { + if (this._rfb_state === 'connect') { + this._updateState('ProtocolVersion', "Starting VNC handshake"); + } else { + this._fail("Got unexpected WebSocket connection"); + } + }.bind(this)); + this._sock.on('close', function (e) { + Util.Warn("WebSocket on-close event"); + var msg = ""; + if (e.code) { + msg = " (code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + if (this._rfb_state === 'disconnect') { + this._updateState('disconnected', 'VNC disconnected' + msg); + } else if (this._rfb_state === 'ProtocolVersion') { + this._fail('Failed to connect to server' + msg); + } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) { + Util.Error("Received onclose while disconnected" + msg); + } else { + this._fail("Server disconnected" + msg); + } + this._sock.off('close'); + }.bind(this)); + this._sock.on('error', function (e) { + Util.Warn("WebSocket on-error event"); + }); + + this._init_vars(); + + var rmode = this._display.get_render_mode(); + if (Websock_native) { + Util.Info("Using native WebSockets"); + this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); + } else { + this._cleanupSocket('fatal'); + throw new Error("WebSocket support is required to use noVNC"); + } + + Util.Debug("<< RFB.constructor"); + }; + + RFB.prototype = { + // Public methods + connect: function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; + + if (!this._rfb_host || !this._rfb_port) { + return this._fail("Must set host and port"); + } + + this._updateState('connect'); + }, + + disconnect: function () { + this._updateState('disconnect', 'Disconnecting'); + this._sock.off('error'); + this._sock.off('message'); + this._sock.off('open'); + }, + + sendPassword: function (passwd) { + this._rfb_password = passwd; + this._rfb_state = 'Authentication'; + setTimeout(this._init_msg.bind(this), 1); + }, + + sendCtrlAltDel: function () { + if (this._rfb_state !== 'normal' || this._view_only) { return false; } + Util.Info("Sending Ctrl-Alt-Del"); + + RFB.messages.keyEvent(this._sock, XK_Control_L, 1); + RFB.messages.keyEvent(this._sock, XK_Alt_L, 1); + RFB.messages.keyEvent(this._sock, XK_Delete, 1); + RFB.messages.keyEvent(this._sock, XK_Delete, 0); + RFB.messages.keyEvent(this._sock, XK_Alt_L, 0); + RFB.messages.keyEvent(this._sock, XK_Control_L, 0); + + this._sock.flush(); + }, + + xvpOp: function (ver, op) { + if (this._rfb_xvp_ver < ver) { return false; } + Util.Info("Sending XVP operation " + op + " (version " + ver + ")"); + this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); + return true; + }, + + xvpShutdown: function () { + return this.xvpOp(1, 2); + }, + + xvpReboot: function () { + return this.xvpOp(1, 3); + }, + + xvpReset: function () { + return this.xvpOp(1, 4); + }, + + // Send a key press. If 'down' is not specified then send a down key + // followed by an up key. + sendKey: function (code, down) { + if (this._rfb_state !== "normal" || this._view_only) { return false; } + if (typeof down !== 'undefined') { + Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); + RFB.messages.keyEvent(this._sock, code, down ? 1 : 0); + } else { + Util.Info("Sending key code (down + up): " + code); + RFB.messages.keyEvent(this._sock, code, 1); + RFB.messages.keyEvent(this._sock, code, 0); + } + + this._sock.flush(); + }, + + clipboardPasteFrom: function (text) { + if (this._rfb_state !== 'normal') { return; } + RFB.messages.clientCutText(this._sock, text); + this._sock.flush(); + }, + + // Requests a change of remote desktop size. This message is an extension + // and may only be sent if we have received an ExtendedDesktopSize message + requestDesktopSize: function (width, height) { + if (this._rfb_state !== "normal") { return; } + + if (this._supportsSetDesktopSize) { + RFB.messages.setDesktopSize(this._sock, width, height, + this._screen_id, this._screen_flags); + this._sock.flush(); + } + }, + + + // Private methods + + _connect: function () { + Util.Debug(">> RFB.connect"); + + var uri; + if (typeof UsingSocketIO !== 'undefined') { + uri = 'http'; + } else { + uri = this._encrypt ? 'wss' : 'ws'; + } + + uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; + Util.Info("connecting to " + uri); + + this._sock.open(uri, this._wsProtocols); + + Util.Debug("<< RFB.connect"); + }, + + _init_vars: function () { + // reset state + this._FBU.rects = 0; + this._FBU.subrects = 0; // RRE and HEXTILE + this._FBU.lines = 0; // RAW + this._FBU.tiles = 0; // HEXTILE + this._FBU.zlibs = []; // TIGHT zlib encoders + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._rfb_tightvnc = false; + + // Clear the per connection encoding stats + var i; + for (i = 0; i < this._encodings.length; i++) { + this._encStats[this._encodings[i][1]][0] = 0; + } + + for (i = 0; i < 4; i++) { + this._FBU.zlibs[i] = new inflator.Inflate(); + } + }, + + _print_stats: function () { + Util.Info("Encoding stats for this connection:"); + var i, s; + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + if (s[0] + s[1] > 0) { + Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects"); + } + } + + Util.Info("Encoding stats since page load:"); + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects"); + } + }, + + _cleanupSocket: function (state) { + if (this._sendTimer) { + clearInterval(this._sendTimer); + this._sendTimer = null; + } + + if (this._msgTimer) { + clearInterval(this._msgTimer); + this._msgTimer = null; + } + + if (this._display && this._display.get_context()) { + this._keyboard.ungrab(); + this._mouse.ungrab(); + if (state !== 'connect' && state !== 'loaded') { + this._display.defaultCursor(); + } + if (Util.get_logging() !== 'debug' || state === 'loaded') { + // Show noVNC logo on load and when disconnected, unless in + // debug mode + this._display.clear(); + } + } + + this._sock.close(); + }, + + /* + * Page states: + * loaded - page load, equivalent to disconnected + * disconnected - idle state + * connect - starting to connect (to ProtocolVersion) + * normal - connected + * disconnect - starting to disconnect + * failed - abnormal disconnect + * fatal - failed to load page, or fatal error + * + * RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * password - waiting for password, not part of RFB + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization (to normal) + */ + _updateState: function (state, statusMsg) { + var oldstate = this._rfb_state; + + if (state === oldstate) { + // Already here, ignore + Util.Debug("Already in state '" + state + "', ignoring"); + } + + /* + * These are disconnected states. A previous connect may + * asynchronously cause a connection so make sure we are closed. + */ + if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1, + 'disconnect': 1, 'failed': 1, 'fatal': 1}) { + this._cleanupSocket(state); + } + + if (oldstate === 'fatal') { + Util.Error('Fatal error, cannot continue'); + } + + var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; + var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg; + if (state === 'failed' || state === 'fatal') { + Util.Error(cmsg); + } else { + Util.Warn(cmsg); + } + + if (oldstate === 'failed' && state === 'disconnected') { + // do disconnect action, but stay in failed state + this._rfb_state = 'failed'; + } else { + this._rfb_state = state; + } + + if (this._disconnTimer && this._rfb_state !== 'disconnect') { + Util.Debug("Clearing disconnect timer"); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + this._sock.off('close'); // make sure we don't get a double event + } + + switch (state) { + case 'normal': + if (oldstate === 'disconnected' || oldstate === 'failed') { + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + } + break; + + case 'connect': + this._init_vars(); + this._connect(); + // WebSocket.onopen transitions to 'ProtocolVersion' + break; + + case 'disconnect': + this._disconnTimer = setTimeout(function () { + this._fail("Disconnect timeout"); + }.bind(this), this._disconnectTimeout * 1000); + + this._print_stats(); + + // WebSocket.onclose transitions to 'disconnected' + break; + + case 'failed': + if (oldstate === 'disconnected') { + Util.Error("Invalid transition from 'disconnected' to 'failed'"); + } else if (oldstate === 'normal') { + Util.Error("Error while connected."); + } else if (oldstate === 'init') { + Util.Error("Error while initializing."); + } + + // Make sure we transition to disconnected + setTimeout(function () { + this._updateState('disconnected'); + }.bind(this), 50); + + break; + + default: + // No state change action to take + } + + if (oldstate === 'failed' && state === 'disconnected') { + this._onUpdateState(this, state, oldstate); + } else { + this._onUpdateState(this, state, oldstate, statusMsg); + } + }, + + _fail: function (msg) { + this._updateState('failed', msg); + return false; + }, + + _handle_message: function () { + if (this._sock.rQlen() === 0) { + Util.Warn("handle_message called on an empty receive queue"); + return; + } + + switch (this._rfb_state) { + case 'disconnected': + case 'failed': + Util.Error("Got data while disconnected"); + break; + case 'normal': + if (this._normal_msg() && this._sock.rQlen() > 0) { + // true means we can continue processing + // Give other events a chance to run + if (this._msgTimer === null) { + Util.Debug("More data to process, creating timer"); + this._msgTimer = setTimeout(function () { + this._msgTimer = null; + this._handle_message(); + }.bind(this), 10); + } else { + Util.Debug("More data to process, existing timer"); + } + } + break; + default: + this._init_msg(); + break; + } + }, + + _handleKeyPress: function (keysym, down) { + if (this._view_only) { return; } // View only, skip keyboard, events + RFB.messages.keyEvent(this._sock, keysym, down); + this._sock.flush(); + }, + + _handleMouseButton: function (x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMask ^= bmask; + } + + if (this._viewportDrag) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': x, 'y': y}; + + // Skip sending mouse events + return; + } else { + this._viewportDragging = false; + + // If the viewport didn't actually move, then treat as a mouse click event + // Send the button down event here, as the button up event is sent at the end of this function + if (!this._viewportHasMoved && !this._view_only) { + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); + } + this._viewportHasMoved = false; + } + } + + if (this._view_only) { return; } // View only, skip mouse events + + if (this._rfb_state !== "normal") { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + }, + + _handleMouseMove: function (x, y) { + if (this._viewportDragging) { + var deltaX = this._viewportDragPos.x - x; + var deltaY = this._viewportDragPos.y - y; + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var dragThreshold = 10 * (window.devicePixelRatio || 1); + + if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || + Math.abs(deltaY) > dragThreshold)) { + this._viewportHasMoved = true; + + this._viewportDragPos = {'x': x, 'y': y}; + this._display.viewportChangePos(deltaX, deltaY); + } + + // Skip sending mouse events + return; + } + + if (this._view_only) { return; } // View only, skip mouse events + + if (this._rfb_state !== "normal") { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + }, + + // Message Handlers + + _negotiate_protocol_version: function () { + if (this._sock.rQlen() < 12) { + return this._fail("Incomplete protocol version"); + } + + var sversion = this._sock.rQshiftStr(12).substr(4, 7); + Util.Info("Server ProtocolVersion: " + sversion); + var is_repeater = 0; + switch (sversion) { + case "000.000": // UltraVNC repeater + is_repeater = 1; + break; + case "003.003": + case "003.006": // UltraVNC + case "003.889": // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case "003.007": + this._rfb_version = 3.7; + break; + case "003.008": + case "004.000": // Intel AMT KVM + case "004.001": // RealVNC 4.6 + this._rfb_version = 3.8; + break; + default: + return this._fail("Invalid server version " + sversion); + } + + if (is_repeater) { + var repeaterID = this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; + } + this._sock.send_string(repeaterID); + return true; + } + + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; + } + + // Send updates either at a rate of 1 update per 50ms, or + // whatever slower rate the network can handle + this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50); + + var cversion = "00" + parseInt(this._rfb_version, 10) + + ".00" + ((this._rfb_version * 10) % 10); + this._sock.send_string("RFB " + cversion + "\n"); + this._updateState('Security', 'Sent ProtocolVersion: ' + cversion); + }, + + _negotiate_security: function () { + if (this._rfb_version >= 3.7) { + // Server sends supported list, client decides + var num_types = this._sock.rQshift8(); + if (this._sock.rQwait("security type", num_types, 1)) { return false; } + + if (num_types === 0) { + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Security failure: " + reason); + } + + this._rfb_auth_scheme = 0; + var types = this._sock.rQshiftBytes(num_types); + Util.Debug("Server security types: " + types); + for (var i = 0; i < types.length; i++) { + if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) { + this._rfb_auth_scheme = types[i]; + } + } + + if (this._rfb_auth_scheme === 0) { + return this._fail("Unsupported security types: " + types); + } + + this._sock.send([this._rfb_auth_scheme]); + } else { + // Server decides + if (this._sock.rQwait("security scheme", 4)) { return false; } + this._rfb_auth_scheme = this._sock.rQshift32(); + } + + this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme); + return this._init_msg(); // jump to authentication + }, + + // authentication + _negotiate_xvp_auth: function () { + var xvp_sep = this._xvp_password_sep; + var xvp_auth = this._rfb_password.split(xvp_sep); + if (xvp_auth.length < 3) { + this._updateState('password', 'XVP credentials required (user' + xvp_sep + + 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password); + this._onPasswordRequired(this); + return false; + } + + var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + + String.fromCharCode(xvp_auth[1].length) + + xvp_auth[0] + + xvp_auth[1]; + this._sock.send_string(xvp_auth_str); + this._rfb_password = xvp_auth.slice(2).join(xvp_sep); + this._rfb_auth_scheme = 2; + return this._negotiate_authentication(); + }, + + _negotiate_std_vnc_auth: function () { + if (this._rfb_password.length === 0) { + // Notify via both callbacks since it's kind of + // an RFB state change and a UI interface issue + this._updateState('password', "Password Required"); + this._onPasswordRequired(this); + return false; + } + + if (this._sock.rQwait("auth challenge", 16)) { return false; } + + // TODO(directxman12): make genDES not require an Array + var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); + var response = RFB.genDES(this._rfb_password, challenge); + this._sock.send(response); + this._updateState("SecurityResult"); + return true; + }, + + _negotiate_tight_tunnels: function (numTunnels) { + var clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } + }; + var serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (var i = 0; i < numTunnels; i++) { + var cap_code = this._sock.rQshift32(); + var cap_vendor = this._sock.rQshiftStr(4); + var cap_signature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || + serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + return this._fail("Client's tunnel type had the incorrect vendor or signature"); + } + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue + } else { + return this._fail("Server wanted tunnels, but doesn't support the notunnel type"); + } + }, + + _negotiate_tight_auth: function () { + if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation + if (this._sock.rQwait("num tunnels", 4)) { return false; } + var numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } + + this._rfb_tightvnc = true; + + if (numTunnels > 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue + } + } + + // second pass, do the sub-auth negotiation + if (this._sock.rQwait("sub auth count", 4)) { return false; } + var subAuthCount = this._sock.rQshift32(); + if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } + + var clientSupportedTypes = { + 'STDVNOAUTH__': 1, + 'STDVVNCAUTH_': 2 + }; + + var serverSupportedTypes = []; + + for (var i = 0; i < subAuthCount; i++) { + var capNum = this._sock.rQshift32(); + var capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } + + for (var authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + + switch (authType) { + case 'STDVNOAUTH__': // no auth + this._updateState('SecurityResult'); + return true; + case 'STDVVNCAUTH_': // VNC auth + this._rfb_auth_scheme = 2; + return this._init_msg(); + default: + return this._fail("Unsupported tiny auth scheme: " + authType); + } + } + } + + this._fail("No supported sub-auth types!"); + }, + + _negotiate_authentication: function () { + switch (this._rfb_auth_scheme) { + case 0: // connection failed + if (this._sock.rQwait("auth reason", 4)) { return false; } + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Auth failure: " + reason); + + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._updateState('SecurityResult'); + return true; + } + this._updateState('ClientInitialisation', "No auth required"); + return this._init_msg(); + + case 22: // XVP auth + return this._negotiate_xvp_auth(); + + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); + + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); + + default: + return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme); + } + }, + + _handle_security_result: function () { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } + switch (this._sock.rQshift32()) { + case 0: // OK + this._updateState('ClientInitialisation', 'Authentication OK'); + return this._init_msg(); + case 1: // failed + if (this._rfb_version >= 3.8) { + var length = this._sock.rQshift32(); + if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } + var reason = this._sock.rQshiftStr(length); + return this._fail(reason); + } else { + return this._fail("Authentication failure"); + } + return false; + case 2: + return this._fail("Too many auth attempts"); + } + }, + + _negotiate_server_init: function () { + if (this._sock.rQwait("server initialization", 24)) { return false; } + + /* Screen size */ + this._fb_width = this._sock.rQshift16(); + this._fb_height = this._sock.rQshift16(); + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); + + /* PIXEL_FORMAT */ + var bpp = this._sock.rQshift8(); + var depth = this._sock.rQshift8(); + var big_endian = this._sock.rQshift8(); + var true_color = this._sock.rQshift8(); + + var red_max = this._sock.rQshift16(); + var green_max = this._sock.rQshift16(); + var blue_max = this._sock.rQshift16(); + var red_shift = this._sock.rQshift8(); + var green_shift = this._sock.rQshift8(); + var blue_shift = this._sock.rQshift8(); + this._sock.rQskipBytes(3); // padding + + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack + + /* Connection name/title */ + var name_length = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', name_length, 24)) { return false; } + this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); + + if (this._rfb_tightvnc) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } + // In TightVNC mode, ServerInit message is extended + var numServerMessages = this._sock.rQshift16(); + var numClientMessages = this._sock.rQshift16(); + var numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding + + var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } + + // we don't actually do anything with the capability information that TIGHT sends, + // so we just skip the all of this. + + // TIGHT server message capabilities + this._sock.rQskipBytes(16 * numServerMessages); + + // TIGHT client message capabilities + this._sock.rQskipBytes(16 * numClientMessages); + + // TIGHT encoding capabilities + this._sock.rQskipBytes(16 * numEncodings); + } + + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + + ", bpp: " + bpp + ", depth: " + depth + + ", big_endian: " + big_endian + + ", true_color: " + true_color + + ", red_max: " + red_max + + ", green_max: " + green_max + + ", blue_max: " + blue_max + + ", red_shift: " + red_shift + + ", green_shift: " + green_shift + + ", blue_shift: " + blue_shift); + + if (big_endian !== 0) { + Util.Warn("Server native endian is not little endian"); + } + + if (red_shift !== 16) { + Util.Warn("Server native red-shift is not 16"); + } + + if (blue_shift !== 0) { + Util.Warn("Server native blue-shift is not 0"); + } + + // we're past the point where we could backtrack, so it's safe to call this + this._onDesktopName(this, this._fb_name); + + if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { + Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); + this._true_color = false; + } + + this._display.set_true_color(this._true_color); + this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); + this._keyboard.grab(); + this._mouse.grab(); + + if (this._true_color) { + this._fb_Bpp = 4; + this._fb_depth = 3; + } else { + this._fb_Bpp = 1; + this._fb_depth = 1; + } + + RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); + RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); + RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); + + this._timing.fbu_rt_start = (new Date()).getTime(); + this._timing.pixels = 0; + this._sock.flush(); + + if (this._encrypt) { + this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); + } else { + this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name); + } + }, + + _init_msg: function () { + switch (this._rfb_state) { + case 'ProtocolVersion': + return this._negotiate_protocol_version(); + + case 'Security': + return this._negotiate_security(); + + case 'Authentication': + return this._negotiate_authentication(); + + case 'SecurityResult': + return this._handle_security_result(); + + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._updateState('ServerInitialisation', "Authentication OK"); + return true; + + case 'ServerInitialisation': + return this._negotiate_server_init(); + } + }, + + _handle_set_colour_map_msg: function () { + Util.Debug("SetColorMapEntries"); + this._sock.rQskip8(); // Padding + + var first_colour = this._sock.rQshift16(); + var num_colours = this._sock.rQshift16(); + if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; } + + for (var c = 0; c < num_colours; c++) { + var red = parseInt(this._sock.rQshift16() / 256, 10); + var green = parseInt(this._sock.rQshift16() / 256, 10); + var blue = parseInt(this._sock.rQshift16() / 256, 10); + this._display.set_colourMap([blue, green, red], first_colour + c); + } + Util.Debug("colourMap: " + this._display.get_colourMap()); + Util.Info("Registered " + num_colours + " colourMap entries"); + + return true; + }, + + _handle_server_cut_text: function () { + Util.Debug("ServerCutText"); + if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + var length = this._sock.rQshift32(); + if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } + + var text = this._sock.rQshiftStr(length); + this._onClipboard(this, text); + + return true; + }, + + _handle_xvp_msg: function () { + if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + var xvp_ver = this._sock.rQshift8(); + var xvp_msg = this._sock.rQshift8(); + + switch (xvp_msg) { + case 0: // XVP_FAIL + this._updateState(this._rfb_state, "Operation Failed"); + break; + case 1: // XVP_INIT + this._rfb_xvp_ver = xvp_ver; + Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); + this._onXvpInit(this._rfb_xvp_ver); + break; + default: + this._fail("Disconnected: illegal server XVP message " + xvp_msg); + break; + } + + return true; + }, + + _normal_msg: function () { + var msg_type; + + if (this._FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = this._sock.rQshift8(); + } + + switch (msg_type) { + case 0: // FramebufferUpdate + var ret = this._framebufferUpdate(); + if (ret) { + RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); + this._sock.flush(); + } + return ret; + + case 1: // SetColorMapEntries + return this._handle_set_colour_map_msg(); + + case 2: // Bell + Util.Debug("Bell"); + this._onBell(this); + return true; + + case 3: // ServerCutText + return this._handle_server_cut_text(); + + case 250: // XVP + return this._handle_xvp_msg(); + + default: + this._fail("Disconnected: illegal server message type " + msg_type); + Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + return true; + } + }, + + _framebufferUpdate: function () { + var ret = true; + var now; + + if (this._FBU.rects === 0) { + if (this._sock.rQwait("FBU header", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + this._FBU.rects = this._sock.rQshift16(); + this._FBU.bytes = 0; + this._timing.cur_fbu = 0; + if (this._timing.fbu_rt_start > 0) { + now = (new Date()).getTime(); + Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); + } + } + + while (this._FBU.rects > 0) { + if (this._rfb_state !== "normal") { return false; } + + if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } + if (this._FBU.bytes === 0) { + if (this._sock.rQwait("rect header", 12)) { return false; } + /* New FramebufferUpdate */ + + var hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + + (hdr[10] << 8) + hdr[11], 10); + + this._onFBUReceive(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + if (!this._encNames[this._FBU.encoding]) { + this._fail("Disconnected: unsupported encoding " + + this._FBU.encoding); + return false; + } + } + + this._timing.last_fbu = (new Date()).getTime(); + + var handler = this._encHandlers[this._FBU.encoding]; + try { + //ret = this._encHandlers[this._FBU.encoding](); + ret = handler(); + } catch (ex) { + console.log("missed " + this._FBU.encoding + ": " + handler); + ret = this._encHandlers[this._FBU.encoding](); + } + + now = (new Date()).getTime(); + this._timing.cur_fbu += (now - this._timing.last_fbu); + + if (ret) { + this._encStats[this._FBU.encoding][0]++; + this._encStats[this._FBU.encoding][1]++; + this._timing.pixels += this._FBU.width * this._FBU.height; + } + + if (this._timing.pixels >= (this._fb_width * this._fb_height)) { + if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || + this._timing.fbu_rt_start > 0) { + this._timing.full_fbu_total += this._timing.cur_fbu; + this._timing.full_fbu_cnt++; + Util.Info("Timing of full FBU, curr: " + + this._timing.cur_fbu + ", total: " + + this._timing.full_fbu_total + ", cnt: " + + this._timing.full_fbu_cnt + ", avg: " + + (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); + } + + if (this._timing.fbu_rt_start > 0) { + var fbu_rt_diff = now - this._timing.fbu_rt_start; + this._timing.fbu_rt_total += fbu_rt_diff; + this._timing.fbu_rt_cnt++; + Util.Info("full FBU round-trip, cur: " + + fbu_rt_diff + ", total: " + + this._timing.fbu_rt_total + ", cnt: " + + this._timing.fbu_rt_cnt + ", avg: " + + (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); + this._timing.fbu_rt_start = 0; + } + } + + if (!ret) { return ret; } // need more data + } + + this._onFBUComplete(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + return true; // We finished this FBU + }, + }; + + Util.make_properties(RFB, [ + ['target', 'wo', 'dom'], // VNC display rendering Canvas object + ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input + ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption + ['true_color', 'rw', 'bool'], // Request true color pixel data + ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor + ['shared', 'rw', 'bool'], // Request shared mode + ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard + ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields + ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection + ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection + ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to + ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags + + // Callback functions + ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change + ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required + ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received + ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received + ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed + ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized + ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received + ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection + ]); + + RFB.prototype.set_local_cursor = function (cursor) { + if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { + this._local_cursor = false; + this._display.disableLocalCursor(); //Only show server-side cursor + } else { + if (this._display.get_cursor_uri()) { + this._local_cursor = true; + } else { + Util.Warn("Browser does not support local cursor"); + this._display.disableLocalCursor(); + } + } + }; + + RFB.prototype.get_display = function () { return this._display; }; + RFB.prototype.get_keyboard = function () { return this._keyboard; }; + RFB.prototype.get_mouse = function () { return this._mouse; }; + + // Class Methods + RFB.messages = { + keyEvent: function (sock, keysym, down) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 4; // msg-type + buff[offset + 1] = down; + + buff[offset + 2] = 0; + buff[offset + 3] = 0; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + sock._sQlen += 8; + }, + + pointerEvent: function (sock, x, y, mask) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 5; // msg-type + + buff[offset + 1] = mask; + + buff[offset + 2] = x >> 8; + buff[offset + 3] = x; + + buff[offset + 4] = y >> 8; + buff[offset + 5] = y; + + sock._sQlen += 6; + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText: function (sock, text) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 6; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + var n = text.length; + + buff[offset + 4] = n >> 24; + buff[offset + 5] = n >> 16; + buff[offset + 6] = n >> 8; + buff[offset + 7] = n; + + for (var i = 0; i < n; i++) { + buff[offset + 8 + i] = text.charCodeAt(i); + } + + sock._sQlen += 8 + n; + }, + + setDesktopSize: function (sock, width, height, id, flags) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 251; // msg-type + buff[offset + 1] = 0; // padding + buff[offset + 2] = width >> 8; // width + buff[offset + 3] = width; + buff[offset + 4] = height >> 8; // height + buff[offset + 5] = height; + + buff[offset + 6] = 1; // number-of-screens + buff[offset + 7] = 0; // padding + + // screen array + buff[offset + 8] = id >> 24; // id + buff[offset + 9] = id >> 16; + buff[offset + 10] = id >> 8; + buff[offset + 11] = id; + buff[offset + 12] = 0; // x-position + buff[offset + 13] = 0; + buff[offset + 14] = 0; // y-position + buff[offset + 15] = 0; + buff[offset + 16] = width >> 8; // width + buff[offset + 17] = width; + buff[offset + 18] = height >> 8; // height + buff[offset + 19] = height; + buff[offset + 20] = flags >> 24; // flags + buff[offset + 21] = flags >> 16; + buff[offset + 22] = flags >> 8; + buff[offset + 23] = flags; + + sock._sQlen += 24; + }, + + pixelFormat: function (sock, bpp, depth, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 0; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = bpp * 8; // bits-per-pixel + buff[offset + 5] = depth * 8; // depth + buff[offset + 6] = 0; // little-endian + buff[offset + 7] = true_color ? 1 : 0; // true-color + + buff[offset + 8] = 0; // red-max + buff[offset + 9] = 255; // red-max + + buff[offset + 10] = 0; // green-max + buff[offset + 11] = 255; // green-max + + buff[offset + 12] = 0; // blue-max + buff[offset + 13] = 255; // blue-max + + buff[offset + 14] = 16; // red-shift + buff[offset + 15] = 8; // green-shift + buff[offset + 16] = 0; // blue-shift + + buff[offset + 17] = 0; // padding + buff[offset + 18] = 0; // padding + buff[offset + 19] = 0; // padding + + sock._sQlen += 20; + }, + + clientEncodings: function (sock, encodings, local_cursor, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 2; // msg-type + buff[offset + 1] = 0; // padding + + // offset + 2 and offset + 3 are encoding count + + var i, j = offset + 4, cnt = 0; + for (i = 0; i < encodings.length; i++) { + if (encodings[i][0] === "Cursor" && !local_cursor) { + Util.Debug("Skipping Cursor pseudo-encoding"); + } else if (encodings[i][0] === "TIGHT" && !true_color) { + // TODO: remove this when we have tight+non-true-color + Util.Warn("Skipping tight as it is only supported with true color"); + } else { + var enc = encodings[i][1]; + buff[j] = enc >> 24; + buff[j + 1] = enc >> 16; + buff[j + 2] = enc >> 8; + buff[j + 3] = enc; + + j += 4; + cnt++; + } + } + + buff[offset + 2] = cnt >> 8; + buff[offset + 3] = cnt; + + sock._sQlen += j - offset; + }, + + fbUpdateRequests: function (sock, cleanDirty, fb_width, fb_height) { + var offsetIncrement = 0; + + var cb = cleanDirty.cleanBox; + var w, h; + if (cb.w > 0 && cb.h > 0) { + w = typeof cb.w === "undefined" ? fb_width : cb.w; + h = typeof cb.h === "undefined" ? fb_height : cb.h; + // Request incremental for clean box + RFB.messages.fbUpdateRequest(sock, 1, cb.x, cb.y, w, h); + } + + for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) { + var db = cleanDirty.dirtyBoxes[i]; + // Force all (non-incremental) for dirty box + w = typeof db.w === "undefined" ? fb_width : db.w; + h = typeof db.h === "undefined" ? fb_height : db.h; + RFB.messages.fbUpdateRequest(sock, 0, db.x, db.y, w, h); + } + }, + + fbUpdateRequest: function (sock, incremental, x, y, w, h) { + var buff = sock._sQ; + var offset = sock._sQlen; + + if (typeof(x) === "undefined") { x = 0; } + if (typeof(y) === "undefined") { y = 0; } + + buff[offset] = 3; // msg-type + buff[offset + 1] = incremental; + + buff[offset + 2] = (x >> 8) & 0xFF; + buff[offset + 3] = x & 0xFF; + + buff[offset + 4] = (y >> 8) & 0xFF; + buff[offset + 5] = y & 0xFF; + + buff[offset + 6] = (w >> 8) & 0xFF; + buff[offset + 7] = w & 0xFF; + + buff[offset + 8] = (h >> 8) & 0xFF; + buff[offset + 9] = h & 0xFF; + + sock._sQlen += 10; + } + }; + + RFB.genDES = function (password, challenge) { + var passwd = []; + for (var i = 0; i < password.length; i++) { + passwd.push(password.charCodeAt(i)); + } + return (new DES(passwd)).encrypt(challenge); + }; + + RFB.extract_data_uri = function (arr) { + return ";base64," + Base64.encode(arr); + }; + + RFB.encodingHandlers = { + RAW: function () { + if (this._FBU.lines === 0) { + this._FBU.lines = this._FBU.height; + } + + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line + if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } + var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); + var curr_height = Math.min(this._FBU.lines, + Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); + this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, + curr_height, this._sock.get_rQ(), + this._sock.get_rQi()); + this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); + this._FBU.lines -= curr_height; + + if (this._FBU.lines > 0) { + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + COPYRECT: function () { + this._FBU.bytes = 4; + if (this._sock.rQwait("COPYRECT", 4)) { return false; } + this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(), + this._FBU.x, this._FBU.y, this._FBU.width, + this._FBU.height); + + this._FBU.rects--; + this._FBU.bytes = 0; + return true; + }, + + RRE: function () { + var color; + if (this._FBU.subrects === 0) { + this._FBU.bytes = 4 + this._fb_Bpp; + if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.subrects = this._sock.rQshift32(); + color = this._sock.rQshiftBytes(this._fb_Bpp); // Background + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); + } + + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { + color = this._sock.rQshiftBytes(this._fb_Bpp); + var x = this._sock.rQshift16(); + var y = this._sock.rQshift16(); + var width = this._sock.rQshift16(); + var height = this._sock.rQshift16(); + this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); + this._FBU.subrects--; + } + + if (this._FBU.subrects > 0) { + var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); + this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + HEXTILE: function () { + var rQ = this._sock.get_rQ(); + var rQi = this._sock.get_rQi(); + + if (this._FBU.tiles === 0) { + this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); + this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); + this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; + this._FBU.tiles = this._FBU.total_tiles; + } + + while (this._FBU.tiles > 0) { + this._FBU.bytes = 1; + if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } + var subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + this._fail("Disconnected: illegal hextile subencoding " + subencoding); + return false; + } + + var subrects = 0; + var curr_tile = this._FBU.total_tiles - this._FBU.tiles; + var tile_x = curr_tile % this._FBU.tiles_x; + var tile_y = Math.floor(curr_tile / this._FBU.tiles_x); + var x = this._FBU.x + tile_x * 16; + var y = this._FBU.y + tile_y * 16; + var w = Math.min(16, (this._FBU.x + this._FBU.width) - x); + var h = Math.min(16, (this._FBU.y + this._FBU.height) - y); + + // Figure out how much we are expecting + if (subencoding & 0x01) { // Raw + this._FBU.bytes += w * h * this._fb_Bpp; + } else { + if (subencoding & 0x02) { // Background + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x04) { // Foreground + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x08) { // AnySubrects + this._FBU.bytes++; // Since we aren't shifting it off + if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } + subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek + if (subencoding & 0x10) { // SubrectsColoured + this._FBU.bytes += subrects * (this._fb_Bpp + 2); + } else { + this._FBU.bytes += subrects * 2; + } + } + } + + if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } + + // We know the encoding and have a whole tile + this._FBU.subencoding = rQ[rQi]; + rQi++; + if (this._FBU.subencoding === 0) { + if (this._FBU.lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Util.Debug(" Ignoring blank after RAW"); + } else { + this._display.fillRect(x, y, w, h, this._FBU.background); + } + } else if (this._FBU.subencoding & 0x01) { // Raw + this._display.blitImage(x, y, w, h, rQ, rQi); + rQi += this._FBU.bytes - 1; + } else { + if (this._FBU.subencoding & 0x02) { // Background + if (this._fb_Bpp == 1) { + this._FBU.background = rQ[rQi]; + } else { + // fb_Bpp is 4 + this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } + rQi += this._fb_Bpp; + } + if (this._FBU.subencoding & 0x04) { // Foreground + if (this._fb_Bpp == 1) { + this._FBU.foreground = rQ[rQi]; + } else { + // this._fb_Bpp is 4 + this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } + rQi += this._fb_Bpp; + } + + this._display.startTile(x, y, w, h, this._FBU.background); + if (this._FBU.subencoding & 0x08) { // AnySubrects + subrects = rQ[rQi]; + rQi++; + + for (var s = 0; s < subrects; s++) { + var color; + if (this._FBU.subencoding & 0x10) { // SubrectsColoured + if (this._fb_Bpp === 1) { + color = rQ[rQi]; + } else { + // _fb_Bpp is 4 + color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } + rQi += this._fb_Bpp; + } else { + color = this._FBU.foreground; + } + var xy = rQ[rQi]; + rQi++; + var sx = (xy >> 4); + var sy = (xy & 0x0f); + + var wh = rQ[rQi]; + rQi++; + var sw = (wh >> 4) + 1; + var sh = (wh & 0x0f) + 1; + + this._display.subTile(sx, sy, sw, sh, color); + } + } + this._display.finishTile(); + } + this._sock.set_rQi(rQi); + this._FBU.lastsubencoding = this._FBU.subencoding; + this._FBU.bytes = 0; + this._FBU.tiles--; + } + + if (this._FBU.tiles === 0) { + this._FBU.rects--; + } + + return true; + }, + + getTightCLength: function (arr) { + var header = 1, data = 0; + data += arr[0] & 0x7f; + if (arr[0] & 0x80) { + header++; + data += (arr[1] & 0x7f) << 7; + if (arr[1] & 0x80) { + header++; + data += arr[2] << 14; + } + } + return [header, data]; + }, + + display_tight: function (isTightPNG) { + if (this._fb_depth === 1) { + this._fail("Tight protocol handler only implements true color mode"); + } + + this._FBU.bytes = 1; // compression-control byte + if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } + + var checksum = function (data) { + var sum = 0; + for (var i = 0; i < data.length; i++) { + sum += data[i]; + if (sum > 65536) sum -= 65536; + } + return sum; + }; + + var resetStreams = 0; + var streamId = -1; + var decompress = function (data, expected) { + for (var i = 0; i < 4; i++) { + if ((resetStreams >> i) & 1) { + this._FBU.zlibs[i].reset(); + console.debug('RESET!'); + Util.Info("Reset zlib stream " + i); + } + } + + //var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + var uncompressed = this._FBU.zlibs[streamId].inflate(data, true, expected); + /*if (uncompressed.status !== 0) { + Util.Error("Invalid data in zlib stream"); + }*/ + + //return uncompressed.data; + return uncompressed; + }.bind(this); + + var indexedToRGBX2Color = function (data, palette, width, height) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + var dest = this._destBuff; + var w = Math.floor((width + 7) / 8); + var w1 = Math.floor(width / 8); + + /*for (var y = 0; y < height; y++) { + var b, x, dp, sp; + var yoffset = y * width; + var ybitoffset = y * w; + var xoffset, targetbyte; + for (x = 0; x < w1; x++) { + xoffset = yoffset + x * 8; + targetbyte = data[ybitoffset + x]; + for (b = 7; b >= 0; b--) { + dp = (xoffset + 7 - b) * 3; + sp = (targetbyte >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + + xoffset = yoffset + x * 8; + targetbyte = data[ybitoffset + x]; + for (b = 7; b >= 8 - width % 8; b--) { + dp = (xoffset + 7 - b) * 3; + sp = (targetbyte >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + }*/ + + for (var y = 0; y < height; y++) { + var b, x, dp, sp; + for (x = 0; x < w1; x++) { + for (b = 7; b >= 0; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + for (b = 7; b >= 8 - width % 8; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + return dest; + }.bind(this); + + var indexedToRGBX = function (data, palette, width, height) { + // Convert indexed (palette based) image data to RGB + var dest = this._destBuff; + var total = width * height * 4; + for (var i = 0, j = 0; i < total; i += 4, j++) { + var sp = data[j] * 3; + dest[i] = palette[sp]; + dest[i + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; + dest[i + 3] = 255; + } + + return dest; + }.bind(this); + + var rQi = this._sock.get_rQi(); + var rQ = this._sock.rQwhole(); + var cmode, data; + var cl_header, cl_data; + + var handlePalette = function () { + var numColors = rQ[rQi + 2] + 1; + var paletteSize = numColors * this._fb_depth; + this._FBU.bytes += paletteSize; + if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } + + var bpp = (numColors <= 2) ? 1 : 8; + var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); + var raw = false; + if (rowSize * this._FBU.height < 12) { + raw = true; + cl_header = 0; + cl_data = rowSize * this._FBU.height; + //clength = [0, rowSize * this._FBU.height]; + } else { + // begin inline getTightCLength (returning two-item arrays is bad for performance with GC) + var cl_offset = rQi + 3 + paletteSize; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + } + + this._FBU.bytes += cl_header + cl_data; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, filter id, num colors, palette entries, and clength off + this._sock.rQskipBytes(3); + //var palette = this._sock.rQshiftBytes(paletteSize); + this._sock.rQshiftTo(this._paletteBuff, paletteSize); + this._sock.rQskipBytes(cl_header); + + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data), rowSize * this._FBU.height); + } + + // Convert indexed (palette based) image data to RGB + var rgbx; + if (numColors == 2) { + rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); + } else { + rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); + } + + + return true; + }.bind(this); + + var handleCopy = function () { + var raw = false; + var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + if (uncompressedSize < 12) { + raw = true; + cl_header = 0; + cl_data = uncompressedSize; + } else { + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + } + this._FBU.bytes = 1 + cl_header + cl_data; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, clength off + this._sock.rQshiftBytes(1 + cl_header); + + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); + } + + this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); + + return true; + }.bind(this); + + var ctl = this._sock.rQpeek8(); + + // Keep tight reset bits + resetStreams = ctl & 0xF; + + // Figure out filter + ctl = ctl >> 4; + streamId = ctl & 0x3; + + if (ctl === 0x08) cmode = "fill"; + else if (ctl === 0x09) cmode = "jpeg"; + else if (ctl === 0x0A) cmode = "png"; + else if (ctl & 0x04) cmode = "filter"; + else if (ctl < 0x04) cmode = "copy"; + else return this._fail("Illegal tight compression received, ctl: " + ctl); + + if (isTightPNG && (cmode === "filter" || cmode === "copy")) { + return this._fail("filter/copy received in tightPNG mode"); + } + + switch (cmode) { + // fill use fb_depth because TPIXELs drop the padding byte + case "fill": // TPIXEL + this._FBU.bytes += this._fb_depth; + break; + case "jpeg": // max clength + this._FBU.bytes += 3; + break; + case "png": // max clength + this._FBU.bytes += 3; + break; + case "filter": // filter id + num colors if palette + this._FBU.bytes += 2; + break; + case "copy": + break; + } + + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Determine FBU.bytes + switch (cmode) { + case "fill": + // skip ctl byte + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); + this._sock.rQskipBytes(4); + break; + case "png": + case "jpeg": + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + this._FBU.bytes = 1 + cl_header + cl_data; // ctl + clength size + jpeg-data + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // We have everything, render it + this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length + var img = new Image(); + img.src = "data: image/" + cmode + + RFB.extract_data_uri(this._sock.rQshiftBytes(cl_data)); + this._display.renderQ_push({ + 'type': 'img', + 'img': img, + 'x': this._FBU.x, + 'y': this._FBU.y + }); + img = null; + break; + case "filter": + var filterId = rQ[rQi + 1]; + if (filterId === 1) { + if (!handlePalette()) { return false; } + } else { + // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter + // Filter 2, Gradient is valid but not use if jpeg is enabled + this._fail("Unsupported tight subencoding received, filter: " + filterId); + } + break; + case "copy": + if (!handleCopy()) { return false; } + break; + } + + + this._FBU.bytes = 0; + this._FBU.rects--; + + return true; + }, + + TIGHT: function () { return this._encHandlers.display_tight(false); }, + TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, + + last_rect: function () { + this._FBU.rects = 0; + return true; + }, + + handle_FB_resize: function () { + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); + this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); + this._timing.fbu_rt_start = (new Date()).getTime(); + + this._FBU.bytes = 0; + this._FBU.rects -= 1; + return true; + }, + + ExtendedDesktopSize: function () { + this._FBU.bytes = 1; + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + + this._supportsSetDesktopSize = true; + var number_of_screens = this._sock.rQpeek8(); + + this._FBU.bytes = 4 + (number_of_screens * 16); + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (var i = 0; i < number_of_screens; i += 1) { + // Save the id and flags of the first screen + if (i === 0) { + this._screen_id = this._sock.rQshiftBytes(4); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screen_flags = this._sock.rQshiftBytes(4); // flags + } else { + this._sock.rQskipBytes(16); + } + } + + /* + * The x-position indicates the reason for the change: + * + * 0 - server resized on its own + * 1 - this client requested the resize + * 2 - another client requested the resize + */ + + // We need to handle errors when we requested the resize. + if (this._FBU.x === 1 && this._FBU.y !== 0) { + var msg = ""; + // The y-position indicates the status code from the server + switch (this._FBU.y) { + case 1: + msg = "Resize is administratively prohibited"; + break; + case 2: + msg = "Out of resources"; + break; + case 3: + msg = "Invalid screen layout"; + break; + default: + msg = "Unknown reason"; + break; + } + Util.Info("Server did not accept the resize request: " + msg); + return true; + } + + this._encHandlers.handle_FB_resize(); + return true; + }, + + DesktopSize: function () { + this._encHandlers.handle_FB_resize(); + return true; + }, + + Cursor: function () { + Util.Debug(">> set_cursor"); + var x = this._FBU.x; // hotspot-x + var y = this._FBU.y; // hotspot-y + var w = this._FBU.width; + var h = this._FBU.height; + + var pixelslength = w * h * this._fb_Bpp; + var masklength = Math.floor((w + 7) / 8) * h; + + this._FBU.bytes = pixelslength + masklength; + if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } + + this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), + this._sock.rQshiftBytes(masklength), + x, y, w, h); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Util.Debug("<< set_cursor"); + return true; + }, + + JPEG_quality_lo: function () { + Util.Error("Server sent jpeg_quality pseudo-encoding"); + }, + + compress_lo: function () { + Util.Error("Server sent compress level pseudo-encoding"); + } + }; +})(); diff --git a/public/novnc/include/ui.js b/public/novnc/include/ui.js new file mode 100644 index 00000000..327c49d3 --- /dev/null +++ b/public/novnc/include/ui.js @@ -0,0 +1,1287 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2016 Samuel Mannehed for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* jslint white: false, browser: true */ +/* global window, $D, Util, WebUtil, RFB, Display */ + +var UI; + +(function () { + "use strict"; + + // Load supporting scripts + window.onscriptsload = function () { UI.load(); }; + Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", + "keysymdef.js", "keyboard.js", "input.js", "display.js", + "rfb.js", "keysym.js", "inflator.js"]); + + UI = { + + rfb_state: 'loaded', + + resizeTimeout: null, + popupStatusTimeout: null, + hideKeyboardTimeout: null, + + settingsOpen: false, + connSettingsOpen: false, + clipboardOpen: false, + keyboardVisible: false, + + isTouchDevice: false, + isSafari: false, + rememberedClipSetting: null, + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, + + shiftDown: false, + ctrlDown: false, + altDown: false, + altGrDown: false, + + // Setup rfb object, load settings from browser storage, then call + // UI.init to setup the UI/menus + load: function(callback) { + WebUtil.initSettings(UI.start, callback); + }, + + // Render default UI and initialize settings menu + start: function(callback) { + UI.isTouchDevice = 'ontouchstart' in document.documentElement; + + // Stylesheet selection dropdown + var sheet = WebUtil.selectStylesheet(); + var sheets = WebUtil.getStylesheets(); + var i; + for (i = 0; i < sheets.length; i += 1) { + UI.addOption($D('noVNC_setting_stylesheet'),sheets[i].title, sheets[i].title); + } + + // Logging selection dropdown + var llevels = ['error', 'warn', 'info', 'debug']; + for (i = 0; i < llevels.length; i += 1) { + UI.addOption($D('noVNC_setting_logging'),llevels[i], llevels[i]); + } + + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + WebUtil.init_logging(UI.getSetting('logging')); + + UI.initSetting('stylesheet', 'default'); + WebUtil.selectStylesheet(null); + // call twice to get around webkit bug + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + + // if port == 80 (or 443) then it won't be present and should be + // set manually + var port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0,5) == 'https') { + port = 443; + } + else if (window.location.protocol.substring(0,4) == 'http') { + port = 80; + } + } + + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('password', ''); + UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('true_color', true); + UI.initSetting('cursor', !UI.isTouchDevice); + UI.initSetting('resize', 'off'); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + UI.initSetting('token', ''); + + var autoconnect = WebUtil.getConfigVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + } + + UI.updateVisualState(); + + $D('noVNC_setting_host').focus(); + + // Show mouse selector buttons on touch screen devices + if (UI.isTouchDevice) { + // Show mobile buttons + $D('noVNC_mobile_buttons').style.display = "inline"; + UI.setMouseButton(); + // Remove the address bar + setTimeout(function() { window.scrollTo(0, 1); }, 100); + UI.forceSetting('clip', true); + } else { + UI.initSetting('clip', false); + } + + UI.setViewClip(); + UI.setBarPosition(); + + Util.addEvent(window, 'resize', function () { + UI.applyResizeMode(); + UI.setViewClip(); + UI.updateViewDrag(); + UI.setBarPosition(); + } ); + + UI.isSafari = (navigator.userAgent.indexOf('Safari') != -1 && + navigator.userAgent.indexOf('Chrome') == -1); + + // Only show the button if fullscreen is properly supported + // * Safari doesn't support alphanumerical input while in fullscreen + if (!UI.isSafari && + (document.documentElement.requestFullscreen || + document.documentElement.mozRequestFullScreen || + document.documentElement.webkitRequestFullscreen || + document.body.msRequestFullscreen)) { + $D('noVNC_fullscreen_button').style.display = "inline"; + Util.addEvent(window, 'fullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'mozfullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'webkitfullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'msfullscreenchange', UI.updateFullscreenButton); + } + + Util.addEvent(window, 'load', UI.keyboardinputReset); + + Util.addEvent(window, 'beforeunload', function () { + if (UI.rfb && UI.rfb_state === 'normal') { + return "You are currently connected."; + } + } ); + + // Show description by default when hosted at for kanaka.github.com + if (location.host === "kanaka.github.io") { + // Open the description dialog + $D('noVNC_description').style.display = "block"; + } else { + // Show the connect panel on first load unless autoconnecting + if (autoconnect === UI.connSettingsOpen) { + UI.toggleConnectPanel(); + } + } + + // Add mouse event click/focus/blur event handlers to the UI + UI.addMouseHandlers(); + + if (typeof callback === "function") { + callback(UI.rfb); + } + }, + + initRFB: function() { + try { + UI.rfb = new RFB({'target': $D('noVNC_canvas'), + 'onUpdateState': UI.updateState, + 'onXvpInit': UI.updateXvpButton, + 'onClipboard': UI.clipboardReceive, + 'onFBUComplete': UI.initialResize, + 'onFBResize': UI.updateViewDrag, + 'onDesktopName': UI.updateDocumentTitle}); + return true; + } catch (exc) { + UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); + return false; + } + }, + + addMouseHandlers: function() { + // Setup interface handlers that can't be inline + $D("noVNC_view_drag_button").onclick = UI.toggleViewDrag; + $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; + $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; + $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; + $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); }; + $D("noVNC_keyboard_button").onclick = UI.showKeyboard; + + $D("noVNC_keyboardinput").oninput = UI.keyInput; + $D("noVNC_keyboardinput").onblur = UI.hideKeyboard; + $D("noVNC_keyboardinput").onsubmit = function () { return false; }; + + $D("noVNC_toggleExtraKeys_button").onclick = UI.toggleExtraKeys; + $D("noVNC_toggleCtrl_button").onclick = UI.toggleCtrl; + $D("noVNC_toggleAlt_button").onclick = UI.toggleAlt; + $D("noVNC_sendTab_button").onclick = UI.sendTab; + $D("noVNC_sendEsc_button").onclick = UI.sendEsc; + + $D("noVNC_sendCtrlAltDel_button").onclick = UI.sendCtrlAltDel; + $D("noVNC_xvpShutdown_button").onclick = function() { UI.rfb.xvpShutdown(); }, + $D("noVNC_xvpReboot_button").onclick = function() { UI.rfb.xvpReboot(); }, + $D("noVNC_xvpReset_button").onclick = function() { UI.rfb.xvpReset(); }, + $D("noVNC_status").onclick = UI.popupStatus; + $D("noVNC_popup_status").onclick = UI.closePopup; + $D("noVNC_toggleXvp_button").onclick = UI.toggleXvpPanel; + $D("noVNC_clipboard_button").onclick = UI.toggleClipboardPanel; + $D("noVNC_fullscreen_button").onclick = UI.toggleFullscreen; + $D("noVNC_settings_button").onclick = UI.toggleSettingsPanel; + $D("noVNC_connectPanel_button").onclick = UI.toggleConnectPanel; + $D("noVNC_disconnect_button").onclick = UI.disconnect; + $D("noVNC_description_button").onclick = UI.toggleConnectPanel; + + $D("noVNC_clipboard_text").onfocus = UI.displayBlur; + $D("noVNC_clipboard_text").onblur = UI.displayFocus; + $D("noVNC_clipboard_text").onchange = UI.clipboardSend; + $D("noVNC_clipboard_clear_button").onclick = UI.clipboardClear; + + $D("noVNC_settings_menu").onmouseover = UI.displayBlur; + $D("noVNC_settings_menu").onmouseover = UI.displayFocus; + $D("noVNC_settings_apply").onclick = UI.settingsApply; + + $D("noVNC_connect_button").onclick = UI.connect; + + $D("noVNC_setting_resize").onchange = UI.enableDisableViewClip; + }, + +/* ------^------- + * /INIT + * ============== + * VISUAL + * ------v------*/ + + updateState: function(rfb, state, oldstate, msg) { + UI.rfb_state = state; + var klass; + switch (state) { + case 'failed': + case 'fatal': + klass = "noVNC_status_error"; + break; + case 'normal': + klass = "noVNC_status_normal"; + break; + case 'disconnected': + $D('noVNC_logo').style.display = "block"; + $D('noVNC_screen').style.display = "none"; + /* falls through */ + case 'loaded': + klass = "noVNC_status_normal"; + break; + case 'password': + UI.toggleConnectPanel(); + + $D('noVNC_connect_button').value = "Send Password"; + $D('noVNC_connect_button').onclick = UI.setPassword; + $D('noVNC_setting_password').focus(); + + klass = "noVNC_status_warn"; + break; + default: + klass = "noVNC_status_warn"; + break; + } + + if (typeof(msg) !== 'undefined') { + $D('noVNC_control_bar').setAttribute("class", klass); + $D('noVNC_status').textContent = msg; + } + + UI.updateVisualState(); + }, + + // Disable/enable controls depending on connection state + updateVisualState: function() { + var connected = UI.rfb && UI.rfb_state === 'normal'; + + //Util.Debug(">> updateVisualState"); + $D('noVNC_setting_encrypt').disabled = connected; + $D('noVNC_setting_true_color').disabled = connected; + if (Util.browserSupportsCursorURIs()) { + $D('noVNC_setting_cursor').disabled = connected; + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_setting_cursor').disabled = true; + } + + UI.enableDisableViewClip(); + $D('noVNC_setting_resize').disabled = connected; + $D('noVNC_setting_shared').disabled = connected; + $D('noVNC_setting_view_only').disabled = connected; + $D('noVNC_setting_path').disabled = connected; + $D('noVNC_setting_repeaterID').disabled = connected; + + if (connected) { + UI.setViewClip(); + UI.setMouseButton(1); + $D('noVNC_clipboard_button').style.display = "inline"; + $D('noVNC_keyboard_button').style.display = "inline"; + $D('noVNC_extra_keys').style.display = ""; + $D('noVNC_sendCtrlAltDel_button').style.display = "inline"; + } else { + UI.setMouseButton(); + $D('noVNC_clipboard_button').style.display = "none"; + $D('noVNC_keyboard_button').style.display = "none"; + $D('noVNC_extra_keys').style.display = "none"; + $D('noVNC_sendCtrlAltDel_button').style.display = "none"; + UI.updateXvpButton(0); + } + + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.updateViewDrag(false); + + switch (UI.rfb_state) { + case 'fatal': + case 'failed': + case 'disconnected': + $D('noVNC_connectPanel_button').style.display = ""; + $D('noVNC_disconnect_button').style.display = "none"; + UI.connSettingsOpen = false; + UI.toggleConnectPanel(); + break; + case 'loaded': + $D('noVNC_connectPanel_button').style.display = ""; + $D('noVNC_disconnect_button').style.display = "none"; + break; + default: + $D('noVNC_connectPanel_button').style.display = "none"; + $D('noVNC_disconnect_button').style.display = ""; + break; + } + + //Util.Debug("<< updateVisualState"); + }, + + popupStatus: function(text) { + var psp = $D('noVNC_popup_status'); + + clearTimeout(UI.popupStatusTimeout); + + if (typeof text === 'string') { + psp.textContent = text; + } else { + psp.textContent = $D('noVNC_status').textContent; + } + psp.style.display = "block"; + psp.style.left = window.innerWidth/2 - + parseInt(window.getComputedStyle(psp).width)/2 -30 + "px"; + + // Show the popup for a maximum of 1.5 seconds + UI.popupStatusTimeout = setTimeout(UI.closePopup, 1500); + }, + + closePopup: function() { + clearTimeout(UI.popupStatusTimeout); + $D('noVNC_popup_status').style.display = "none"; + }, + +/* ------^------- + * /VISUAL + * ============== + * SETTINGS + * ------v------*/ + + // Initial page load read/initialization of settings + initSetting: function(name, defVal) { + // Check Query string followed by cookie + var val = WebUtil.getConfigVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + UI.updateSetting(name, val); + return val; + }, + + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting: function(name, value) { + + // Save the cookie for this session + if (typeof value !== 'undefined') { + WebUtil.writeSetting(name, value); + } + + // Update the settings control + value = UI.getSetting(name); + + var ctrl = $D('noVNC_setting_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + + } else if (typeof ctrl.options !== 'undefined') { + for (var i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; + } + } + } else { + /*Weird IE9 error leads to 'null' appearring + in textboxes instead of ''.*/ + if (value === null) { + value = ""; + } + ctrl.value = value; + } + }, + + // Save control setting to cookie + saveSetting: function(name) { + var val, ctrl = $D('noVNC_setting_' + name); + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Util.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, + + // Force a setting to be a certain value + forceSetting: function(name, val) { + UI.updateSetting(name, val); + return val; + }, + + // Read form control compatible setting from cookie + getSetting: function(name) { + var ctrl = $D('noVNC_setting_' + name); + var val = WebUtil.readSetting(name); + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { + val = false; + } else { + val = true; + } + } + return val; + }, + + // Save/apply settings when 'Apply' button is pressed + settingsApply: function() { + //Util.Debug(">> settingsApply"); + UI.saveSetting('encrypt'); + UI.saveSetting('true_color'); + if (Util.browserSupportsCursorURIs()) { + UI.saveSetting('cursor'); + } + + UI.saveSetting('resize'); + + if (UI.getSetting('resize') === 'downscale' || UI.getSetting('resize') === 'scale') { + UI.forceSetting('clip', false); + } + + UI.saveSetting('clip'); + UI.saveSetting('shared'); + UI.saveSetting('view_only'); + UI.saveSetting('path'); + UI.saveSetting('repeaterID'); + UI.saveSetting('stylesheet'); + UI.saveSetting('logging'); + + // Settings with immediate (non-connected related) effect + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + WebUtil.init_logging(UI.getSetting('logging')); + UI.setViewClip(); + UI.updateViewDrag(); + //Util.Debug("<< settingsApply"); + }, + + // Open menu + openSettingsMenu: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + $D('noVNC_settings').style.display = "block"; + $D('noVNC_settings_button').className = "noVNC_status_button_selected"; + UI.settingsOpen = true; + }, + + // Close menu (without applying settings) + closeSettingsMenu: function() { + $D('noVNC_settings').style.display = "none"; + $D('noVNC_settings_button').className = "noVNC_status_button"; + UI.settingsOpen = false; + }, + + // Toggle the settings menu: + // On open, settings are refreshed from saved cookies. + // On close, settings are applied + toggleSettingsPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + if (UI.settingsOpen) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } else { + UI.updateSetting('encrypt'); + UI.updateSetting('true_color'); + if (Util.browserSupportsCursorURIs()) { + UI.updateSetting('cursor'); + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_setting_cursor').disabled = true; + } + UI.updateSetting('clip'); + UI.updateSetting('resize'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('stylesheet'); + UI.updateSetting('logging'); + + UI.openSettingsMenu(); + } + }, + +/* ------^------- + * /SETTINGS + * ============== + * XVP + * ------v------*/ + + // Show the XVP panel + toggleXvpPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Toggle XVP panel + if (UI.xvpOpen === true) { + $D('noVNC_xvp').style.display = "none"; + $D('noVNC_toggleXvp_button').className = "noVNC_status_button"; + UI.xvpOpen = false; + } else { + $D('noVNC_xvp').style.display = "block"; + $D('noVNC_toggleXvp_button').className = "noVNC_status_button_selected"; + UI.xvpOpen = true; + } + }, + + // Disable/enable XVP button + updateXvpButton: function(ver) { + if (ver >= 1) { + $D('noVNC_toggleXvp_button').style.display = 'inline'; + } else { + $D('noVNC_toggleXvp_button').style.display = 'none'; + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + } + }, + +/* ------^------- + * /XVP + * ============== + * CLIPBOARD + * ------v------*/ + + // Show the clipboard panel + toggleClipboardPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + // Toggle Clipboard Panel + if (UI.clipboardOpen === true) { + $D('noVNC_clipboard').style.display = "none"; + $D('noVNC_clipboard_button').className = "noVNC_status_button"; + UI.clipboardOpen = false; + } else { + $D('noVNC_clipboard').style.display = "block"; + $D('noVNC_clipboard_button').className = "noVNC_status_button_selected"; + UI.clipboardOpen = true; + } + }, + + clipboardReceive: function(rfb, text) { + Util.Debug(">> UI.clipboardReceive: " + text.substr(0,40) + "..."); + $D('noVNC_clipboard_text').value = text; + Util.Debug("<< UI.clipboardReceive"); + }, + + clipboardClear: function() { + $D('noVNC_clipboard_text').value = ""; + UI.rfb.clipboardPasteFrom(""); + }, + + clipboardSend: function() { + var text = $D('noVNC_clipboard_text').value; + Util.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Util.Debug("<< UI.clipboardSend"); + }, + +/* ------^------- + * /CLIPBOARD + * ============== + * CONNECTION + * ------v------*/ + + // Show the connection settings panel/menu + toggleConnectPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close connection settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + $D('noVNC_connectPanel_button').className = "noVNC_status_button"; + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + + // Toggle Connection Panel + if (UI.connSettingsOpen === true) { + $D('noVNC_controls').style.display = "none"; + $D('noVNC_connectPanel_button').className = "noVNC_status_button"; + UI.connSettingsOpen = false; + UI.saveSetting('host'); + UI.saveSetting('port'); + UI.saveSetting('token'); + //UI.saveSetting('password'); + } else { + $D('noVNC_controls').style.display = "block"; + $D('noVNC_connectPanel_button').className = "noVNC_status_button_selected"; + UI.connSettingsOpen = true; + $D('noVNC_setting_host').focus(); + } + }, + + connect: function() { + UI.closeSettingsMenu(); + UI.toggleConnectPanel(); + + var host = $D('noVNC_setting_host').value; + var port = $D('noVNC_setting_port').value; + var password = $D('noVNC_setting_password').value; + var token = $D('noVNC_setting_token').value; + var path = $D('noVNC_setting_path').value; + + //if token is in path then ignore the new token variable + if (token) { + path = WebUtil.injectParamIfMissing(path, "token", token); + } + + if ((!host) || (!port)) { + throw new Error("Must set host and port"); + } + + if (!UI.initRFB()) return; + + UI.rfb.set_encrypt(UI.getSetting('encrypt')); + UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + UI.rfb.set_shared(UI.getSetting('shared')); + UI.rfb.set_view_only(UI.getSetting('view_only')); + UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + + UI.rfb.connect(host, port, password, path); + + //Close dialog. + setTimeout(UI.setBarPosition, 100); + $D('noVNC_logo').style.display = "none"; + $D('noVNC_screen').style.display = "inline"; + }, + + disconnect: function() { + UI.closeSettingsMenu(); + UI.rfb.disconnect(); + + // Restore the callback used for initial resize + UI.rfb.set_onFBUComplete(UI.initialResize); + + $D('noVNC_logo').style.display = "block"; + $D('noVNC_screen').style.display = "none"; + + // Don't display the connection settings until we're actually disconnected + }, + + setPassword: function() { + UI.rfb.sendPassword($D('noVNC_setting_password').value); + //Reset connect button. + $D('noVNC_connect_button').value = "Connect"; + $D('noVNC_connect_button').onclick = UI.connect; + //Hide connection panel. + UI.toggleConnectPanel(); + return false; + }, + +/* ------^------- + * /CONNECTION + * ============== + * FULLSCREEN + * ------v------*/ + + toggleFullscreen: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (document.body.msRequestFullscreen) { + document.body.msRequestFullscreen(); + } + } + UI.enableDisableViewClip(); + UI.updateFullscreenButton(); + }, + + updateFullscreenButton: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement ) { + $D('noVNC_fullscreen_button').className = "noVNC_status_button_selected"; + } else { + $D('noVNC_fullscreen_button').className = "noVNC_status_button"; + } + }, + +/* ------^------- + * /FULLSCREEN + * ============== + * RESIZE + * ------v------*/ + + // Apply remote resizing or local scaling + applyResizeMode: function() { + if (!UI.rfb) return; + + var screen = UI.screenSize(); + + if (screen && UI.rfb_state === 'normal' && UI.rfb.get_display()) { + + var display = UI.rfb.get_display(); + var resizeMode = UI.getSetting('resize'); + + if (resizeMode === 'remote') { + + // Request changing the resolution of the remote display to + // the size of the local browser viewport. + + // In order to not send multiple requests before the browser-resize + // is finished we wait 0.5 seconds before sending the request. + clearTimeout(UI.resizeTimeout); + UI.resizeTimeout = setTimeout(function(){ + + // Limit the viewport to the size of the browser window + display.set_maxWidth(screen.w); + display.set_maxHeight(screen.h); + + Util.Debug('Attempting requestDesktopSize(' + + screen.w + ', ' + screen.h + ')'); + + // Request a remote size covering the viewport + UI.rfb.requestDesktopSize(screen.w, screen.h); + }, 500); + + } else if (resizeMode === 'scale' || resizeMode === 'downscale') { + var downscaleOnly = resizeMode === 'downscale'; + var scaleRatio = display.autoscale(screen.w, screen.h, downscaleOnly); + UI.rfb.get_mouse().set_scale(scaleRatio); + Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); + } + } + }, + + // The screen is always the same size as the available viewport + // in the browser window minus the height of the control bar + screenSize: function() { + var screen = $D('noVNC_screen'); + + // Hide the scrollbars until the size is calculated + screen.style.overflow = "hidden"; + + var pos = Util.getPosition(screen); + var w = pos.width; + var h = pos.height; + + screen.style.overflow = "visible"; + + if (isNaN(w) || isNaN(h)) { + return false; + } else { + return {w: w, h: h}; + } + }, + + // Normally we only apply the current resize mode after a window resize + // event. This means that when a new connection is opened, there is no + // resize mode active. + // We have to wait until the first FBU because this is where the client + // will find the supported encodings of the server. Some calls later in + // the chain is dependant on knowing the server-capabilities. + initialResize: function(rfb, fbu) { + UI.applyResizeMode(); + // After doing this once, we remove the callback. + UI.rfb.set_onFBUComplete(function() { }); + }, + +/* ------^------- + * /RESIZE + * ============== + * CLIPPING + * ------v------*/ + + // Set and configure viewport clipping + setViewClip: function(clip) { + var display; + if (UI.rfb) { + display = UI.rfb.get_display(); + } else { + UI.forceSetting('clip', clip); + return; + } + + var cur_clip = display.get_viewport(); + + if (typeof(clip) !== 'boolean') { + // Use current setting + clip = UI.getSetting('clip'); + } + + if (clip && !cur_clip) { + // Turn clipping on + UI.updateSetting('clip', true); + } else if (!clip && cur_clip) { + // Turn clipping off + UI.updateSetting('clip', false); + display.set_viewport(false); + // Disable max dimensions + display.set_maxWidth(0); + display.set_maxHeight(0); + display.viewportChangeSize(); + } + if (UI.getSetting('clip')) { + // If clipping, update clipping settings + display.set_viewport(true); + + var size = UI.screenSize(); + if (size) { + display.set_maxWidth(size.w); + display.set_maxHeight(size.h); + + // Hide potential scrollbars that can skew the position + $D('noVNC_screen').style.overflow = "hidden"; + + // The x position marks the left margin of the canvas, + // remove the margin from both sides to keep it centered + var new_w = size.w - (2 * Util.getPosition($D('noVNC_canvas')).x); + + $D('noVNC_screen').style.overflow = "visible"; + + display.viewportChangeSize(new_w, size.h); + } + } + }, + + // Handle special cases where clipping is forced on/off or locked + enableDisableViewClip: function() { + var resizeSetting = $D('noVNC_setting_resize'); + var connected = UI.rfb && UI.rfb_state === 'normal'; + + if (UI.isSafari) { + // Safari auto-hides the scrollbars which makes them + // impossible to use in most cases + UI.setViewClip(true); + $D('noVNC_setting_clip').disabled = true; + } else if (resizeSetting.value === 'downscale' || resizeSetting.value === 'scale') { + // Disable clipping if we are scaling + UI.setViewClip(false); + $D('noVNC_setting_clip').disabled = true; + } else if (document.msFullscreenElement) { + // The browser is IE and we are in fullscreen mode. + // - We need to force clipping while in fullscreen since + // scrollbars doesn't work. + UI.popupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); + UI.rememberedClipSetting = UI.getSetting('clip'); + UI.setViewClip(true); + $D('noVNC_setting_clip').disabled = true; + } else if (document.body.msRequestFullscreen && UI.rememberedClip !== null) { + // Restore view clip to what it was before fullscreen on IE + UI.setViewClip(UI.rememberedClipSetting); + $D('noVNC_setting_clip').disabled = connected || UI.isTouchDevice; + } else { + $D('noVNC_setting_clip').disabled = connected || UI.isTouchDevice; + if (UI.isTouchDevice) { + UI.setViewClip(true); + } + } + }, + +/* ------^------- + * /CLIPPING + * ============== + * VIEWDRAG + * ------v------*/ + + // Update the viewport drag state + updateViewDrag: function(drag) { + if (!UI.rfb) return; + + var viewDragButton = $D('noVNC_view_drag_button'); + + // Check if viewport drag is possible. It is only possible + // if the remote display is clipping the client display. + if (UI.rfb_state === 'normal' && + UI.rfb.get_display().get_viewport() && + UI.rfb.get_display().clippingDisplay()) { + + viewDragButton.style.display = "inline"; + viewDragButton.disabled = false; + + } else { + // The size of the remote display is the same or smaller + // than the client display. Make sure viewport drag isn't + // active when it can't be used. + if (UI.rfb.get_viewportDrag) { + viewDragButton.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + + // The button is disabled instead of hidden on touch devices + if (UI.rfb_state === 'normal' && UI.isTouchDevice) { + viewDragButton.style.display = "inline"; + viewDragButton.disabled = true; + } else { + viewDragButton.style.display = "none"; + } + return; + } + + if (typeof(drag) !== "undefined" && + typeof(drag) !== "object") { + if (drag) { + viewDragButton.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } else { + viewDragButton.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + } + }, + + toggleViewDrag: function() { + if (!UI.rfb) return; + + var viewDragButton = $D('noVNC_view_drag_button'); + if (UI.rfb.get_viewportDrag()) { + viewDragButton.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } else { + viewDragButton.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } + }, + +/* ------^------- + * /VIEWDRAG + * ============== + * KEYBOARD + * ------v------*/ + + // On touch devices, show the OS keyboard + showKeyboard: function() { + var kbi = $D('noVNC_keyboardinput'); + var skb = $D('noVNC_keyboard_button'); + var l = kbi.value.length; + if(UI.keyboardVisible === false) { + kbi.focus(); + try { kbi.setSelectionRange(l, l); } // Move the caret to the end + catch (err) {} // setSelectionRange is undefined in Google Chrome + UI.keyboardVisible = true; + skb.className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === true) { + kbi.blur(); + skb.className = "noVNC_status_button"; + UI.keyboardVisible = false; + } + }, + + hideKeyboard: function() { + $D('noVNC_keyboard_button').className = "noVNC_status_button"; + //Weird bug in iOS if you change keyboardVisible + //here it does not actually occur so next time + //you click keyboard icon it doesnt work. + UI.hideKeyboardTimeout = setTimeout(function() { + UI.keyboardVisible = false; + },100); + }, + + keepKeyboard: function() { + clearTimeout(UI.hideKeyboardTimeout); + if(UI.keyboardVisible === true) { + $D('noVNC_keyboardinput').focus(); + $D('noVNC_keyboard_button').className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === false) { + $D('noVNC_keyboardinput').blur(); + $D('noVNC_keyboard_button').className = "noVNC_status_button"; + } + }, + + keyboardinputReset: function() { + var kbi = $D('noVNC_keyboardinput'); + kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); + UI.lastKeyboardinput = kbi.value; + }, + + // When normal keyboard events are left uncought, use the input events from + // the keyboardinput element instead and generate the corresponding key events. + // This code is required since some browsers on Android are inconsistent in + // sending keyCodes in the normal keyboard events when using on screen keyboards. + keyInput: function(event) { + + if (!UI.rfb) return; + + var newValue = event.target.value; + + if (!UI.lastKeyboardinput) { + UI.keyboardinputReset(); + } + var oldValue = UI.lastKeyboardinput; + + var newLen; + try { + // Try to check caret position since whitespace at the end + // will not be considered by value.length in some browsers + newLen = Math.max(event.target.selectionStart, newValue.length); + } catch (err) { + // selectionStart is undefined in Google Chrome + newLen = newValue.length; + } + var oldLen = oldValue.length; + + var backspaces; + var inputs = newLen - oldLen; + if (inputs < 0) { + backspaces = -inputs; + } else { + backspaces = 0; + } + + // Compare the old string with the new to account for + // text-corrections or other input that modify existing text + var i; + for (i = 0; i < Math.min(oldLen, newLen); i++) { + if (newValue.charAt(i) != oldValue.charAt(i)) { + inputs = newLen - i; + backspaces = oldLen - i; + break; + } + } + + // Send the key events + for (i = 0; i < backspaces; i++) { + UI.rfb.sendKey(XK_BackSpace); + } + for (i = newLen - inputs; i < newLen; i++) { + UI.rfb.sendKey(newValue.charCodeAt(i)); + } + + // Control the text content length in the keyboardinput element + if (newLen > 2 * UI.defaultKeyboardinputLen) { + UI.keyboardinputReset(); + } else if (newLen < 1) { + // There always have to be some text in the keyboardinput + // element with which backspace can interact. + UI.keyboardinputReset(); + // This sometimes causes the keyboard to disappear for a second + // but it is required for the android keyboard to recognize that + // text has been added to the field + event.target.blur(); + // This has to be ran outside of the input handler in order to work + setTimeout(UI.keepKeyboard, 0); + } else { + UI.lastKeyboardinput = newValue; + } + }, + + toggleExtraKeys: function() { + UI.keepKeyboard(); + if(UI.extraKeysVisible === false) { + $D('noVNC_toggleCtrl_button').style.display = "inline"; + $D('noVNC_toggleAlt_button').style.display = "inline"; + $D('noVNC_sendTab_button').style.display = "inline"; + $D('noVNC_sendEsc_button').style.display = "inline"; + $D('noVNC_toggleExtraKeys_button').className = "noVNC_status_button_selected"; + UI.extraKeysVisible = true; + } else if(UI.extraKeysVisible === true) { + $D('noVNC_toggleCtrl_button').style.display = ""; + $D('noVNC_toggleAlt_button').style.display = ""; + $D('noVNC_sendTab_button').style.display = ""; + $D('noVNC_sendEsc_button').style.display = ""; + $D('noVNC_toggleExtraKeys_button').className = "noVNC_status_button"; + UI.extraKeysVisible = false; + } + }, + + sendEsc: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Escape); + }, + + sendTab: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Tab); + }, + + toggleCtrl: function() { + UI.keepKeyboard(); + if(UI.ctrlOn === false) { + UI.rfb.sendKey(XK_Control_L, true); + $D('noVNC_toggleCtrl_button').className = "noVNC_status_button_selected"; + UI.ctrlOn = true; + } else if(UI.ctrlOn === true) { + UI.rfb.sendKey(XK_Control_L, false); + $D('noVNC_toggleCtrl_button').className = "noVNC_status_button"; + UI.ctrlOn = false; + } + }, + + toggleAlt: function() { + UI.keepKeyboard(); + if(UI.altOn === false) { + UI.rfb.sendKey(XK_Alt_L, true); + $D('noVNC_toggleAlt_button').className = "noVNC_status_button_selected"; + UI.altOn = true; + } else if(UI.altOn === true) { + UI.rfb.sendKey(XK_Alt_L, false); + $D('noVNC_toggleAlt_button').className = "noVNC_status_button"; + UI.altOn = false; + } + }, + + sendCtrlAltDel: function() { + UI.rfb.sendCtrlAltDel(); + }, + +/* ------^------- + * /KEYBOARD + * ============== + * MISC + * ------v------*/ + + setMouseButton: function(num) { + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; + } + if (UI.rfb) { + UI.rfb.get_mouse().set_touchButton(num); + } + + var blist = [0, 1,2,4]; + for (var b = 0; b < blist.length; b++) { + var button = $D('noVNC_mouse_button' + blist[b]); + if (blist[b] === num) { + button.style.display = ""; + } else { + button.style.display = "none"; + } + } + }, + + displayBlur: function() { + if (!UI.rfb) return; + + UI.rfb.get_keyboard().set_focused(false); + UI.rfb.get_mouse().set_focused(false); + }, + + displayFocus: function() { + if (!UI.rfb) return; + + UI.rfb.get_keyboard().set_focused(true); + UI.rfb.get_mouse().set_focused(true); + }, + + // Display the desktop name in the document title + updateDocumentTitle: function(rfb, name) { + document.title = name + " - noVNC"; + }, + + //Helper to add options to dropdown. + addOption: function(selectbox, text, value) { + var optn = document.createElement("OPTION"); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, + + setBarPosition: function() { + $D('noVNC_control_bar').style.top = (window.pageYOffset) + 'px'; + $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; + + var vncwidth = $D('noVNC_container').style.offsetWidth; + $D('noVNC_control_bar').style.width = vncwidth + 'px'; + } + +/* ------^------- + * /MISC + * ============== + */ + }; +})(); diff --git a/public/novnc/include/util.js b/public/novnc/include/util.js new file mode 100644 index 00000000..ed0e3cde --- /dev/null +++ b/public/novnc/include/util.js @@ -0,0 +1,622 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* jshint white: false, nonstandard: true */ +/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */ + +// Globals defined here +var Util = {}; + + +/* + * Make arrays quack + */ + +var addFunc = function (cl, name, func) { + if (!cl.prototype[name]) { + Object.defineProperty(cl.prototype, name, { enumerable: false, value: func }); + } +}; + +addFunc(Array, 'push8', function (num) { + "use strict"; + this.push(num & 0xFF); +}); + +addFunc(Array, 'push16', function (num) { + "use strict"; + this.push((num >> 8) & 0xFF, + num & 0xFF); +}); + +addFunc(Array, 'push32', function (num) { + "use strict"; + this.push((num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + num & 0xFF); +}); + +// IE does not support map (even in IE9) +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Array, 'map', function (fun /*, thisp*/) { + "use strict"; + var len = this.length; + if (typeof fun != "function") { + throw new TypeError(); + } + + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) { + res[i] = fun.call(thisp, this[i], i, this); + } + } + + return res; +}); + +// IE <9 does not support indexOf +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Array, 'indexOf', function (elt /*, from*/) { + "use strict"; + var len = this.length >>> 0; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } + + for (; from < len; from++) { + if (from in this && + this[from] === elt) { + return from; + } + } + return -1; +}); + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +if (!Object.keys) { + Object.keys = (function () { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + })(); +} + +// PhantomJS 1.x doesn't support bind, +// so leave this in until PhantomJS 2.0 is released +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +addFunc(Function, 'bind', function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError("Function.prototype.bind - " + + "what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; +}); + +// +// requestAnimationFrame shim with setTimeout fallback +// + +window.requestAnimFrame = (function () { + "use strict"; + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + window.setTimeout(callback, 1000 / 60); + }; +})(); + +/* + * ------------------------------------------------------ + * Namespaced in Util + * ------------------------------------------------------ + */ + +/* + * Logging/debug routines + */ + +Util._log_level = 'warn'; +Util.init_logging = function (level) { + "use strict"; + if (typeof level === 'undefined') { + level = Util._log_level; + } else { + Util._log_level = level; + } + if (typeof window.console === "undefined") { + if (typeof window.opera !== "undefined") { + window.console = { + 'log' : window.opera.postError, + 'warn' : window.opera.postError, + 'error': window.opera.postError + }; + } else { + window.console = { + 'log' : function (m) {}, + 'warn' : function (m) {}, + 'error': function (m) {} + }; + } + } + + Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; + /* jshint -W086 */ + switch (level) { + case 'debug': + Util.Debug = function (msg) { console.log(msg); }; + case 'info': + Util.Info = function (msg) { console.log(msg); }; + case 'warn': + Util.Warn = function (msg) { console.warn(msg); }; + case 'error': + Util.Error = function (msg) { console.error(msg); }; + case 'none': + break; + default: + throw new Error("invalid logging type '" + level + "'"); + } + /* jshint +W086 */ +}; +Util.get_logging = function () { + return Util._log_level; +}; +// Initialize logging level +Util.init_logging(); + +Util.make_property = function (proto, name, mode, type) { + "use strict"; + + var getter; + if (type === 'arr') { + getter = function (idx) { + if (typeof idx !== 'undefined') { + return this['_' + name][idx]; + } else { + return this['_' + name]; + } + }; + } else { + getter = function () { + return this['_' + name]; + }; + } + + var make_setter = function (process_val) { + if (process_val) { + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = process_val(val); + } else { + this['_' + name] = process_val(val); + } + }; + } else { + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = val; + } else { + this['_' + name] = val; + } + }; + } + }; + + var setter; + if (type === 'bool') { + setter = make_setter(function (val) { + if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) { + return false; + } else { + return true; + } + }); + } else if (type === 'int') { + setter = make_setter(function (val) { return parseInt(val, 10); }); + } else if (type === 'float') { + setter = make_setter(parseFloat); + } else if (type === 'str') { + setter = make_setter(String); + } else if (type === 'func') { + setter = make_setter(function (val) { + if (!val) { + return function () {}; + } else { + return val; + } + }); + } else if (type === 'arr' || type === 'dom' || type == 'raw') { + setter = make_setter(); + } else { + throw new Error('Unknown property type ' + type); // some sanity checking + } + + // set the getter + if (typeof proto['get_' + name] === 'undefined') { + proto['get_' + name] = getter; + } + + // set the setter if needed + if (typeof proto['set_' + name] === 'undefined') { + if (mode === 'rw') { + proto['set_' + name] = setter; + } else if (mode === 'wo') { + proto['set_' + name] = function (val, idx) { + if (typeof this['_' + name] !== 'undefined') { + throw new Error(name + " can only be set once"); + } + setter.call(this, val, idx); + }; + } + } + + // make a special setter that we can use in set defaults + proto['_raw_set_' + name] = function (val, idx) { + setter.call(this, val, idx); + //delete this['_init_set_' + name]; // remove it after use + }; +}; + +Util.make_properties = function (constructor, arr) { + "use strict"; + for (var i = 0; i < arr.length; i++) { + Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]); + } +}; + +Util.set_defaults = function (obj, conf, defaults) { + var defaults_keys = Object.keys(defaults); + var conf_keys = Object.keys(conf); + var keys_obj = {}; + var i; + for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; } + for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; } + var keys = Object.keys(keys_obj); + + for (i = 0; i < keys.length; i++) { + var setter = obj['_raw_set_' + keys[i]]; + if (!setter) { + Util.Warn('Invalid property ' + keys[i]); + continue; + } + + if (keys[i] in conf) { + setter.call(obj, conf[keys[i]]); + } else { + setter.call(obj, defaults[keys[i]]); + } + } +}; + +/* + * Decode from UTF-8 + */ +Util.decodeUTF8 = function (utf8string) { + "use strict"; + return decodeURIComponent(escape(utf8string)); +}; + + + +/* + * Cross-browser routines + */ + + +// Dynamically load scripts without using document.write() +// Reference: http://unixpapa.com/js/dyna.html +// +// Handles the case where load_scripts is invoked from a script that +// itself is loaded via load_scripts. Once all scripts are loaded the +// window.onscriptsloaded handler is called (if set). +Util.get_include_uri = function () { + return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/"; +}; +Util._loading_scripts = []; +Util._pending_scripts = []; +Util.load_scripts = function (files) { + "use strict"; + var head = document.getElementsByTagName('head')[0], script, + ls = Util._loading_scripts, ps = Util._pending_scripts; + + var loadFunc = function (e) { + while (ls.length > 0 && (ls[0].readyState === 'loaded' || + ls[0].readyState === 'complete')) { + // For IE, append the script to trigger execution + var s = ls.shift(); + //console.log("loaded script: " + s.src); + head.appendChild(s); + } + if (!this.readyState || + (Util.Engine.presto && this.readyState === 'loaded') || + this.readyState === 'complete') { + if (ps.indexOf(this) >= 0) { + this.onload = this.onreadystatechange = null; + //console.log("completed script: " + this.src); + ps.splice(ps.indexOf(this), 1); + + // Call window.onscriptsload after last script loads + if (ps.length === 0 && window.onscriptsload) { + window.onscriptsload(); + } + } + } + }; + + for (var f = 0; f < files.length; f++) { + script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = Util.get_include_uri() + files[f]; + //console.log("loading script: " + script.src); + script.onload = script.onreadystatechange = loadFunc; + // In-order script execution tricks + if (Util.Engine.trident) { + // For IE wait until readyState is 'loaded' before + // appending it which will trigger execution + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + ls.push(script); + } else { + // For webkit and firefox set async=false and append now + // https://developer.mozilla.org/en-US/docs/HTML/Element/script + script.async = false; + head.appendChild(script); + } + ps.push(script); + } +}; + + +Util.getPosition = function(obj) { + "use strict"; + // NB(sross): the Mozilla developer reference seems to indicate that + // getBoundingClientRect includes border and padding, so the canvas + // style should NOT include either. + var objPosition = obj.getBoundingClientRect(); + return {'x': objPosition.left + window.pageXOffset, 'y': objPosition.top + window.pageYOffset, + 'width': objPosition.width, 'height': objPosition.height}; +}; + + +// Get mouse event position in DOM element +Util.getEventPosition = function (e, obj, scale) { + "use strict"; + var evt, docX, docY, pos; + //if (!e) evt = window.event; + evt = (e ? e : window.event); + evt = (evt.changedTouches ? evt.changedTouches[0] : evt.touches ? evt.touches[0] : evt); + if (evt.pageX || evt.pageY) { + docX = evt.pageX; + docY = evt.pageY; + } else if (evt.clientX || evt.clientY) { + docX = evt.clientX + document.body.scrollLeft + + document.documentElement.scrollLeft; + docY = evt.clientY + document.body.scrollTop + + document.documentElement.scrollTop; + } + pos = Util.getPosition(obj); + if (typeof scale === "undefined") { + scale = 1; + } + var realx = docX - pos.x; + var realy = docY - pos.y; + var x = Math.max(Math.min(realx, pos.width - 1), 0); + var y = Math.max(Math.min(realy, pos.height - 1), 0); + return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; +}; + + +// Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events +Util.addEvent = function (obj, evType, fn) { + "use strict"; + if (obj.attachEvent) { + var r = obj.attachEvent("on" + evType, fn); + return r; + } else if (obj.addEventListener) { + obj.addEventListener(evType, fn, false); + return true; + } else { + throw new Error("Handler could not be attached"); + } +}; + +Util.removeEvent = function (obj, evType, fn) { + "use strict"; + if (obj.detachEvent) { + var r = obj.detachEvent("on" + evType, fn); + return r; + } else if (obj.removeEventListener) { + obj.removeEventListener(evType, fn, false); + return true; + } else { + throw new Error("Handler could not be removed"); + } +}; + +Util.stopEvent = function (e) { + "use strict"; + if (e.stopPropagation) { e.stopPropagation(); } + else { e.cancelBubble = true; } + + if (e.preventDefault) { e.preventDefault(); } + else { e.returnValue = false; } +}; + +Util._cursor_uris_supported = null; + +Util.browserSupportsCursorURIs = function () { + if (Util._cursor_uris_supported === null) { + try { + var target = document.createElement('canvas'); + target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; + + if (target.style.cursor) { + Util.Info("Data URI scheme cursor supported"); + Util._cursor_uris_supported = true; + } else { + Util.Warn("Data URI scheme cursor not supported"); + Util._cursor_uris_supported = false; + } + } catch (exc) { + Util.Error("Data URI scheme cursor test exception: " + exc); + Util._cursor_uris_supported = false; + } + } + + return Util._cursor_uris_supported; +}; + +// Set browser engine versions. Based on mootools. +Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; + +(function () { + "use strict"; + // 'presto': (function () { return (!window.opera) ? false : true; }()), + var detectPresto = function () { + return !!window.opera; + }; + + // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); + var detectTrident = function () { + if (!window.ActiveXObject) { + return false; + } else { + if (window.XMLHttpRequest) { + return (document.querySelectorAll) ? 6 : 5; + } else { + return 4; + } + } + }; + + // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), + var detectInitialWebkit = function () { + try { + if (navigator.taintEnabled) { + return false; + } else { + if (Util.Features.xpath) { + return (Util.Features.query) ? 525 : 420; + } else { + return 419; + } + } + } catch (e) { + return false; + } + }; + + var detectActualWebkit = function (initial_ver) { + var re = /WebKit\/([0-9\.]*) /; + var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1]; + return parseFloat(str_ver, 10); + }; + + // 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }()) + var detectGecko = function () { + /* jshint -W041 */ + if (!document.getBoxObjectFor && window.mozInnerScreenX == null) { + return false; + } else { + return (document.getElementsByClassName) ? 19 : 18; + } + /* jshint +W041 */ + }; + + Util.Engine = { + // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) + //'presto': (function() { + // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), + 'presto': detectPresto(), + 'trident': detectTrident(), + 'webkit': detectInitialWebkit(), + 'gecko': detectGecko(), + }; + + if (Util.Engine.webkit) { + // Extract actual webkit version if available + Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit); + } +})(); + +Util.Flash = (function () { + "use strict"; + var v, version; + try { + v = navigator.plugins['Shockwave Flash'].description; + } catch (err1) { + try { + v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); + } catch (err2) { + v = '0 r0'; + } + } + version = v.match(/\d+/g); + return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; +}()); diff --git a/public/novnc/include/websock.js b/public/novnc/include/websock.js new file mode 100644 index 00000000..4d7a4f9a --- /dev/null +++ b/public/novnc/include/websock.js @@ -0,0 +1,440 @@ +/* + * Websock: high-performance binary WebSockets + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * Websock is similar to the standard WebSocket object but Websock + * enables communication with raw TCP sockets (i.e. the binary stream) + * via websockify. This is accomplished by base64 encoding the data + * stream between Websock and websockify. + * + * Websock has built-in receive queue buffering; the message event + * does not contain actual data but is simply a notification that + * there is new data available. Several rQ* methods are available to + * read binary data off of the receive queue. + */ + +/*jslint browser: true, bitwise: true */ +/*global Util*/ + + +// Load Flash WebSocket emulator if needed + +// To force WebSocket emulator even when native WebSocket available +//window.WEB_SOCKET_FORCE_FLASH = true; +// To enable WebSocket emulator debug: +//window.WEB_SOCKET_DEBUG=1; + +if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { + Websock_native = true; +} else if (window.MozWebSocket && !window.WEB_SOCKET_FORCE_FLASH) { + Websock_native = true; + window.WebSocket = window.MozWebSocket; +} else { + /* no builtin WebSocket so load web_socket.js */ + + Websock_native = false; +} + +function Websock() { + "use strict"; + + this._websocket = null; // WebSocket object + + this._rQi = 0; // Receive queue index + this._rQlen = 0; // Next write position in the receive queue + this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) + this._rQmax = this._rQbufferSize / 8; + // called in init: this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ = null; // Receive queue + + this._sQbufferSize = 1024 * 10; // 10 KiB + // called in init: this._sQ = new Uint8Array(this._sQbufferSize); + this._sQlen = 0; + this._sQ = null; // Send queue + + this._mode = 'binary'; // Current WebSocket mode: 'binary', 'base64' + this.maxBufferedAmount = 200; + + this._eventHandlers = { + 'message': function () {}, + 'open': function () {}, + 'close': function () {}, + 'error': function () {} + }; +} + +(function () { + "use strict"; + // this has performance issues in some versions Chromium, and + // doesn't gain a tremendous amount of performance increase in Firefox + // at the moment. It may be valuable to turn it on in the future. + var ENABLE_COPYWITHIN = false; + + var MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB + + var typedArrayToString = (function () { + // This is only for PhantomJS, which doesn't like apply-ing + // with Typed Arrays + try { + var arr = new Uint8Array([1, 2, 3]); + String.fromCharCode.apply(null, arr); + return function (a) { return String.fromCharCode.apply(null, a); }; + } catch (ex) { + return function (a) { + return String.fromCharCode.apply( + null, Array.prototype.slice.call(a)); + }; + } + })(); + + Websock.prototype = { + // Getters and Setters + get_sQ: function () { + return this._sQ; + }, + + get_rQ: function () { + return this._rQ; + }, + + get_rQi: function () { + return this._rQi; + }, + + set_rQi: function (val) { + this._rQi = val; + }, + + // Receive Queue + rQlen: function () { + return this._rQlen - this._rQi; + }, + + rQpeek8: function () { + return this._rQ[this._rQi]; + }, + + rQshift8: function () { + return this._rQ[this._rQi++]; + }, + + rQskip8: function () { + this._rQi++; + }, + + rQskipBytes: function (num) { + this._rQi += num; + }, + + // TODO(directxman12): test performance with these vs a DataView + rQshift16: function () { + return (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + rQshift32: function () { + return (this._rQ[this._rQi++] << 24) + + (this._rQ[this._rQi++] << 16) + + (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + rQshiftStr: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + var arr = new Uint8Array(this._rQ.buffer, this._rQi, len); + this._rQi += len; + return typedArrayToString(arr); + }, + + rQshiftBytes: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + this._rQi += len; + return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + }, + + rQshiftTo: function (target, len) { + if (len === undefined) { len = this.rQlen(); } + // TODO: make this just use set with views when using a ArrayBuffer to store the rQ + target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); + this._rQi += len; + }, + + rQwhole: function () { + return new Uint8Array(this._rQ.buffer, 0, this._rQlen); + }, + + rQslice: function (start, end) { + if (end) { + return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + } else { + return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); + } + }, + + // Check to see if we must wait for 'num' bytes (default to FBU.bytes) + // to be available in the receive queue. Return true if we need to + // wait (and possibly print a debug message), otherwise false. + rQwait: function (msg, num, goback) { + var rQlen = this._rQlen - this._rQi; // Skip rQlen() function call + if (rQlen < num) { + if (goback) { + if (this._rQi < goback) { + throw new Error("rQwait cannot backup " + goback + " bytes"); + } + this._rQi -= goback; + } + return true; // true means need more data + } + return false; + }, + + // Send Queue + + flush: function () { + if (this._websocket.bufferedAmount !== 0) { + Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); + } + + if (this._websocket.bufferedAmount < this.maxBufferedAmount) { + if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { + this._websocket.send(this._encode_message()); + this._sQlen = 0; + } + + return true; + } else { + Util.Info("Delaying send, bufferedAmount: " + + this._websocket.bufferedAmount); + return false; + } + }, + + send: function (arr) { + this._sQ.set(arr, this._sQlen); + this._sQlen += arr.length; + return this.flush(); + }, + + send_string: function (str) { + this.send(str.split('').map(function (chr) { + return chr.charCodeAt(0); + })); + }, + + // Event Handlers + off: function (evt) { + this._eventHandlers[evt] = function () {}; + }, + + on: function (evt, handler) { + this._eventHandlers[evt] = handler; + }, + + _allocate_buffers: function () { + this._rQ = new Uint8Array(this._rQbufferSize); + this._sQ = new Uint8Array(this._sQbufferSize); + }, + + init: function (protocols, ws_schema) { + this._allocate_buffers(); + this._rQi = 0; + this._websocket = null; + + // Check for full typed array support + var bt = false; + if (('Uint8Array' in window) && + ('set' in Uint8Array.prototype)) { + bt = true; + } + + // Check for full binary type support in WebSockets + // Inspired by: + // https://github.com/Modernizr/Modernizr/issues/370 + // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js + var wsbt = false; + try { + if (bt && ('binaryType' in WebSocket.prototype || + !!(new WebSocket(ws_schema + '://.').binaryType))) { + Util.Info("Detected binaryType support in WebSockets"); + wsbt = true; + } + } catch (exc) { + // Just ignore failed test localhost connection + } + + // Default protocols if not specified + if (typeof(protocols) === "undefined") { + protocols = 'binary'; + } + + if (Array.isArray(protocols) && protocols.indexOf('binary') > -1) { + protocols = 'binary'; + } + + if (!wsbt) { + throw new Error("noVNC no longer supports base64 WebSockets. " + + "Please use a browser which supports binary WebSockets."); + } + + if (protocols != 'binary') { + throw new Error("noVNC no longer supports base64 WebSockets. Please " + + "use the binary subprotocol instead."); + } + + return protocols; + }, + + open: function (uri, protocols) { + var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; + protocols = this.init(protocols, ws_schema); + + this._websocket = new WebSocket(uri, protocols); + + if (protocols.indexOf('binary') >= 0) { + this._websocket.binaryType = 'arraybuffer'; + } + + this._websocket.onmessage = this._recv_message.bind(this); + this._websocket.onopen = (function () { + Util.Debug('>> WebSock.onopen'); + if (this._websocket.protocol) { + this._mode = this._websocket.protocol; + Util.Info("Server choose sub-protocol: " + this._websocket.protocol); + } else { + this._mode = 'binary'; + Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); + } + + if (this._mode != 'binary') { + throw new Error("noVNC no longer supports base64 WebSockets. Please " + + "use the binary subprotocol instead."); + + } + + this._eventHandlers.open(); + Util.Debug("<< WebSock.onopen"); + }).bind(this); + this._websocket.onclose = (function (e) { + Util.Debug(">> WebSock.onclose"); + this._eventHandlers.close(e); + Util.Debug("<< WebSock.onclose"); + }).bind(this); + this._websocket.onerror = (function (e) { + Util.Debug(">> WebSock.onerror: " + e); + this._eventHandlers.error(e); + Util.Debug("<< WebSock.onerror: " + e); + }).bind(this); + }, + + close: function () { + if (this._websocket) { + if ((this._websocket.readyState === WebSocket.OPEN) || + (this._websocket.readyState === WebSocket.CONNECTING)) { + Util.Info("Closing WebSocket connection"); + this._websocket.close(); + } + + this._websocket.onmessage = function (e) { return; }; + } + }, + + // private methods + _encode_message: function () { + // Put in a binary arraybuffer + // according to the spec, you can send ArrayBufferViews with the send method + return new Uint8Array(this._sQ.buffer, 0, this._sQlen); + }, + + _expand_compact_rQ: function (min_fit) { + var resizeNeeded = min_fit || this._rQlen - this._rQi > this._rQbufferSize / 2; + if (resizeNeeded) { + if (!min_fit) { + // just double the size if we need to do compaction + this._rQbufferSize *= 2; + } else { + // otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8 + this._rQbufferSize = (this._rQlen - this._rQi + min_fit) * 8; + } + } + + // we don't want to grow unboundedly + if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { + this._rQbufferSize = MAX_RQ_GROW_SIZE; + if (this._rQbufferSize - this._rQlen - this._rQi < min_fit) { + throw new Exception("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); + } + } + + if (resizeNeeded) { + var old_rQbuffer = this._rQ.buffer; + this._rQmax = this._rQbufferSize / 8; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); + } else { + if (ENABLE_COPYWITHIN) { + this._rQ.copyWithin(0, this._rQi); + } else { + this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); + } + } + + this._rQlen = this._rQlen - this._rQi; + this._rQi = 0; + }, + + _decode_message: function (data) { + // push arraybuffer values onto the end + var u8 = new Uint8Array(data); + if (u8.length > this._rQbufferSize - this._rQlen) { + this._expand_compact_rQ(u8.length); + } + this._rQ.set(u8, this._rQlen); + this._rQlen += u8.length; + }, + + _recv_message: function (e) { + try { + this._decode_message(e.data); + if (this.rQlen() > 0) { + this._eventHandlers.message(); + // Compact the receive queue + if (this._rQlen == this._rQi) { + this._rQlen = 0; + this._rQi = 0; + } else if (this._rQlen > this._rQmax) { + this._expand_compact_rQ(); + } + } else { + Util.Debug("Ignoring empty message"); + } + } catch (exc) { + var exception_str = ""; + if (exc.name) { + exception_str += "\n name: " + exc.name + "\n"; + exception_str += " message: " + exc.message + "\n"; + } + + if (typeof exc.description !== 'undefined') { + exception_str += " description: " + exc.description + "\n"; + } + + if (typeof exc.stack !== 'undefined') { + exception_str += exc.stack; + } + + if (exception_str.length > 0) { + Util.Error("recv_message, caught exception: " + exception_str); + } else { + Util.Error("recv_message, caught exception: " + exc); + } + + if (typeof exc.name !== 'undefined') { + this._eventHandlers.error(exc.name + ": " + exc.message); + } else { + this._eventHandlers.error(exc); + } + } + } + }; +})(); diff --git a/public/novnc/include/webutil.js b/public/novnc/include/webutil.js new file mode 100644 index 00000000..9ee34730 --- /dev/null +++ b/public/novnc/include/webutil.js @@ -0,0 +1,292 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2013 NTT corp. + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/*jslint bitwise: false, white: false, browser: true, devel: true */ +/*global Util, window, document */ + +// Globals defined here +var WebUtil = {}, $D; + +/* + * Simple DOM selector by ID + */ +if (!window.$D) { + window.$D = function (id) { + if (document.getElementById) { + return document.getElementById(id); + } else if (document.all) { + return document.all[id]; + } else if (document.layers) { + return document.layers[id]; + } + return undefined; + }; +} + + +/* + * ------------------------------------------------------ + * Namespaced in WebUtil + * ------------------------------------------------------ + */ + +// init log level reading the logging HTTP param +WebUtil.init_logging = function (level) { + "use strict"; + if (typeof level !== "undefined") { + Util._log_level = level; + } else { + var param = document.location.href.match(/logging=([A-Za-z0-9\._\-]*)/); + Util._log_level = (param || ['', Util._log_level])[1]; + } + Util.init_logging(); +}; + + +WebUtil.dirObj = function (obj, depth, parent) { + "use strict"; + if (! depth) { depth = 2; } + if (! parent) { parent = ""; } + + // Print the properties of the passed-in object + var msg = ""; + for (var i in obj) { + if ((depth > 1) && (typeof obj[i] === "object")) { + // Recurse attributes that are objects + msg += WebUtil.dirObj(obj[i], depth - 1, parent + "." + i); + } else { + //val = new String(obj[i]).replace("\n", " "); + var val = ""; + if (typeof(obj[i]) === "undefined") { + val = "undefined"; + } else { + val = obj[i].toString().replace("\n", " "); + } + if (val.length > 30) { + val = val.substr(0, 30) + "..."; + } + msg += parent + "." + i + ": " + val + "\n"; + } + } + return msg; +}; + +// Read a query string variable +WebUtil.getQueryVar = function (name, defVal) { + "use strict"; + var re = new RegExp('.*[?&]' + name + '=([^&#]*)'), + match = document.location.href.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + if (match) { + return decodeURIComponent(match[1]); + } else { + return defVal; + } +}; + +// Read a hash fragment variable +WebUtil.getHashVar = function (name, defVal) { + "use strict"; + var re = new RegExp('.*[&#]' + name + '=([^&]*)'), + match = document.location.hash.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + if (match) { + return decodeURIComponent(match[1]); + } else { + return defVal; + } +}; + +// Read a variable from the fragment or the query string +// Fragment takes precedence +WebUtil.getConfigVar = function (name, defVal) { + "use strict"; + var val = WebUtil.getHashVar(name); + if (val === null) { + val = WebUtil.getQueryVar(name, defVal); + } + return val; +}; + +/* + * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html + */ + +// No days means only for this browser session +WebUtil.createCookie = function (name, value, days) { + "use strict"; + var date, expires; + if (days) { + date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } else { + expires = ""; + } + + var secure; + if (document.location.protocol === "https:") { + secure = "; secure"; + } else { + secure = ""; + } + document.cookie = name + "=" + value + expires + "; path=/" + secure; +}; + +WebUtil.readCookie = function (name, defaultValue) { + "use strict"; + var nameEQ = name + "=", + ca = document.cookie.split(';'); + + for (var i = 0; i < ca.length; i += 1) { + var c = ca[i]; + while (c.charAt(0) === ' ') { c = c.substring(1, c.length); } + if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length); } + } + return (typeof defaultValue !== 'undefined') ? defaultValue : null; +}; + +WebUtil.eraseCookie = function (name) { + "use strict"; + WebUtil.createCookie(name, "", -1); +}; + +/* + * Setting handling. + */ + +WebUtil.initSettings = function (callback /*, ...callbackArgs */) { + "use strict"; + var callbackArgs = Array.prototype.slice.call(arguments, 1); + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.get(function (cfg) { + WebUtil.settings = cfg; + console.log(WebUtil.settings); + if (callback) { + callback.apply(this, callbackArgs); + } + }); + } else { + // No-op + if (callback) { + callback.apply(this, callbackArgs); + } + } +}; + +// No days means only for this browser session +WebUtil.writeSetting = function (name, value) { + "use strict"; + if (window.chrome && window.chrome.storage) { + //console.log("writeSetting:", name, value); + if (WebUtil.settings[name] !== value) { + WebUtil.settings[name] = value; + window.chrome.storage.sync.set(WebUtil.settings); + } + } else { + localStorage.setItem(name, value); + } +}; + +WebUtil.readSetting = function (name, defaultValue) { + "use strict"; + var value; + if (window.chrome && window.chrome.storage) { + value = WebUtil.settings[name]; + } else { + value = localStorage.getItem(name); + } + if (typeof value === "undefined") { + value = null; + } + if (value === null && typeof defaultValue !== undefined) { + return defaultValue; + } else { + return value; + } +}; + +WebUtil.eraseSetting = function (name) { + "use strict"; + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.remove(name); + delete WebUtil.settings[name]; + } else { + localStorage.removeItem(name); + } +}; + +/* + * Alternate stylesheet selection + */ +WebUtil.getStylesheets = function () { + "use strict"; + var links = document.getElementsByTagName("link"); + var sheets = []; + + for (var i = 0; i < links.length; i += 1) { + if (links[i].title && + links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { + sheets.push(links[i]); + } + } + return sheets; +}; + +// No sheet means try and use value from cookie, null sheet used to +// clear all alternates. +WebUtil.selectStylesheet = function (sheet) { + "use strict"; + if (typeof sheet === 'undefined') { + sheet = 'default'; + } + + var sheets = WebUtil.getStylesheets(); + for (var i = 0; i < sheets.length; i += 1) { + var link = sheets[i]; + if (link.title === sheet) { + Util.Debug("Using stylesheet " + sheet); + link.disabled = false; + } else { + //Util.Debug("Skipping stylesheet " + link.title); + link.disabled = true; + } + } + return sheet; +}; + +WebUtil.injectParamIfMissing = function (path, param, value) { + // force pretend that we're dealing with a relative path + // (assume that we wanted an extra if we pass one in) + path = "/" + path; + + var elem = document.createElement('a'); + elem.href = path; + + var param_eq = encodeURIComponent(param) + "="; + var query; + if (elem.search) { + query = elem.search.slice(1).split('&'); + } else { + query = []; + } + + if (!query.some(function (v) { return v.startsWith(param_eq); })) { + query.push(param_eq + encodeURIComponent(value)); + elem.search = "?" + query.join("&"); + } + + // some browsers (e.g. IE11) may occasionally omit the leading slash + // in the elem.pathname string. Handle that case gracefully. + if (elem.pathname.charAt(0) == "/") { + return elem.pathname.slice(1) + elem.search + elem.hash; + } else { + return elem.pathname + elem.search + elem.hash; + } +}; diff --git a/public/novnc/karma.conf.js b/public/novnc/karma.conf.js new file mode 100644 index 00000000..870b8551 --- /dev/null +++ b/public/novnc/karma.conf.js @@ -0,0 +1,197 @@ +// Karma configuration + +module.exports = function(config) { + /*var customLaunchers = { + sl_chrome_win7: { + base: 'SauceLabs', + browserName: 'chrome', + platform: 'Windows 7' + }, + + sl_firefox30_linux: { + base: 'SauceLabs', + browserName: 'firefox', + version: '30', + platform: 'Linux' + }, + + sl_firefox26_linux: { + base: 'SauceLabs', + browserName: 'firefox', + version: 26, + platform: 'Linux' + }, + + sl_windows7_ie10: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 7', + version: '10' + }, + + sl_windows81_ie11: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 8.1', + version: '11' + }, + + sl_osxmavericks_safari7: { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.9', + version: '7' + }, + + sl_osxmtnlion_safari6: { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.8', + version: '6' + } + };*/ + + var customLaunchers = {}; + var browsers = []; + var useSauce = false; + + if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) { + useSauce = true; + } + + if (useSauce && process.env.TEST_BROWSER_NAME && process.env.TEST_BROWSER_NAME != 'PhantomJS') { + var names = process.env.TEST_BROWSER_NAME.split(','); + var platforms = process.env.TEST_BROWSER_OS.split(','); + var versions = []; + if (process.env.TEST_BROWSER_VERSION) { + versions = process.env.TEST_BROWSER_VERSION.split(','); + } else { + versions = [null]; + } + + for (var i = 0; i < names.length; i++) { + for (var j = 0; j < platforms.length; j++) { + for (var k = 0; k < versions.length; k++) { + var launcher_name = 'sl_' + platforms[j].replace(/[^a-zA-Z0-9]/g, '') + '_' + names[i]; + if (versions[k]) { + launcher_name += '_' + versions[k]; + } + + customLaunchers[launcher_name] = { + base: 'SauceLabs', + browserName: names[i], + platform: platforms[j], + }; + + if (versions[i]) { + customLaunchers[launcher_name].version = versions[k]; + } + } + } + } + + browsers = Object.keys(customLaunchers); + } else { + useSauce = false; + browsers = ['PhantomJS']; + } + + var my_conf = { + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'sinon', 'chai', 'sinon-chai'], + + + // list of files / patterns to load in the browser (loaded in order) + files: [ + 'tests/fake.*.js', + 'tests/assertions.js', + 'include/util.js', // load first to avoid issues, since methods are called immediately + //'../include/*.js', + 'include/base64.js', + 'include/keysym.js', + 'include/keysymdef.js', + 'include/keyboard.js', + 'include/input.js', + 'include/websock.js', + 'include/rfb.js', + 'include/des.js', + 'include/display.js', + 'include/inflator.js', + 'tests/test.*.js' + ], + + client: { + mocha: { + 'ui': 'bdd' + } + }, + + // list of files to exclude + exclude: [ + '../include/playback.js', + '../include/ui.js' + ], + + customLaunchers: customLaunchers, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: browsers, + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha', 'saucelabs'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Increase timeout in case connection is slow/we run more browsers than possible + // (we currently get 3 for free, and we try to run 7, so it can take a while) + captureTimeout: 240000, + + // similarly to above + browserNoActivityTimeout: 100000, + }; + + if (useSauce) { + my_conf.captureTimeout = 0; // use SL timeout + my_conf.sauceLabs = { + testName: 'noVNC Tests (all)', + startConnect: false, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER + }; + } + + config.set(my_conf); +}; diff --git a/public/novnc/package.json b/public/novnc/package.json new file mode 100644 index 00000000..18afa551 --- /dev/null +++ b/public/novnc/package.json @@ -0,0 +1,50 @@ +{ + "name": "noVNC", + "version": "0.6.2", + "description": "An HTML5 VNC client", + "main": "karma.conf.js", + "directories": { + "doc": "docs", + "test": "tests" + }, + "scripts": { + "test": "PATH=$PATH:node_modules/karma/bin karma start karma.conf.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/kanaka/noVNC.git" + }, + "author": "Joel Martin (https://github.com/kanaka)", + "contributors": [ + "Solly Ross (https://github.com/directxman12)", + "Peter Åstrand (https://github.com/astrand)", + "Samuel Mannehed (https://github.com/samhed)" + ], + "license": "MPL 2.0", + "bugs": { + "url": "https://github.com/kanaka/noVNC/issues" + }, + "homepage": "https://github.com/kanaka/noVNC", + "devDependencies": { + "ansi": "^0.3.0", + "casperjs": "^1.1.0-beta3", + "chai": "^2.1.0", + "commander": "^2.6.0", + "karma": "^0.12.31", + "karma-chai": "^0.1.0", + "karma-mocha": "^0.1.10", + "karma-mocha-reporter": "^1.0.0", + "karma-phantomjs-launcher": "^0.1.4", + "karma-sauce-launcher": "^0.2.10", + "karma-sinon": "^1.0.4", + "karma-sinon-chai-latest": "^0.1.0", + "mocha": "^2.1.0", + "open": "^0.0.5", + "phantom": "^0.7.2", + "phantomjs": "^1.9.15", + "sinon": "^1.12.2", + "sinon-chai": "^2.7.0", + "spooky": "^0.2.5", + "temp": "^0.8.1" + } +} diff --git a/public/novnc/tests/arrays.html b/public/novnc/tests/arrays.html new file mode 100644 index 00000000..257df261 --- /dev/null +++ b/public/novnc/tests/arrays.html @@ -0,0 +1,39 @@ + + + + Javascript Arrays Performance Test + + + + + + + + +

    Javascript Arrays Performance Test

    + Iterations:   + Array Size: *1024  + +   + +

    + Results:
    + +
    + + + + + + diff --git a/public/novnc/tests/arrays.js b/public/novnc/tests/arrays.js new file mode 100644 index 00000000..843df7df --- /dev/null +++ b/public/novnc/tests/arrays.js @@ -0,0 +1,375 @@ +/* + * Javascript binary array performance tests + * Copyright (C) 2012 Joel Martin + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +var ctx, i, j, randlist, + new_normal, new_imageData, new_arrayBuffer, + browser = Browser.browser + " " + + Browser.version + " on " + + Browser.OS, + do_imageData = false, + do_arrayBuffer = false, + conf = { + 'create_cnt' : 2000, + 'read_cnt' : 5000000, + 'write_cnt' : 5000000, + 'iterations' : 0, + 'order_l1' : [browser], + 'order_l2' : ['normal', + 'imageData', + 'arrayBuffer'], + 'order_l3' : ['create', + 'sequentialRead', + 'randomRead', + 'sequentialWrite'] + }, + stats = {}, + testFunc = {}, + iteration, arraySize; + +var newline = "\n"; +if (Util.Engine.trident) { + var newline = "
    \n"; +} +function message(str) { + //console.log(str); + cell = $D('messages'); + cell.textContent += str + newline; + cell.scrollTop = cell.scrollHeight; +} + +function vmessage(str) { + if (verbose) { + message(str); + } else { + console.log(str); + } +} + +new_normal = function() { + var arr = [], i; + for (i = 0; i < arraySize; i++) { + arr[i] = 0; + } + return arr; +} + +/* Will be overridden with real function */ +new_imageData = function() { + throw("imageData not supported"); +}; + +new_imageData_createImageData = function() { + var imageData = ctx.createImageData(1024/4, arraySize / 1024); + return imageData.data; +}; + +new_imageData_getImageData = function() { + var imageData = ctx.getImageData(0, 0, 1024/4, arraySize / 1024), + arr = imageData.data; + for (i = 0; i < arraySize; i++) { + arr[i] = 0; + } + return arr; +}; + +new_arrayBuffer = function() { + var arr = new ArrayBuffer(arraySize); + return new Uint8Array(arr); +} + +function init_randlist() { + randlist = []; + for (var i=0; i < arraySize; i++) { + randlist[i] = parseInt(Math.random() * 256, 10); + } +} +function copy_randlist(arr) { + for (var i=0; i < arraySize; i++) { + arr[i] = randlist[i]; + } +} + +function begin() { + var i, j; + conf.iterations = parseInt($D('iterations').value, 10); + arraySize = parseInt($D('arraySize').value, 10) * 1024; + + init_randlist(); + + // TODO: randomize test_list + + stats = {}; + for (i = 0; i < conf.order_l2.length; i++) { + stats[conf.order_l2[i]] = {}; + for (j = 0; j < conf.order_l3.length; j++) { + stats[conf.order_l2[i]][conf.order_l3[j]] = []; + } + } + + $D('startButton').value = "Running"; + $D('startButton').disabled = true; + + message("running " + conf.iterations + " test iterations"); + iteration = 1; + setTimeout(run_next_iteration, 250); +} + +function finish() { + var totalTime, arrayType, testType, times; + message("tests finished"); + + for (j = 0; j < conf.order_l3.length; j++) { + testType = conf.order_l3[j]; + message("Test '" + testType + "'"); + for (i = 0; i < conf.order_l2.length; i++) { + arrayType = conf.order_l2[i]; + message(" Array Type '" + arrayType); + times = stats[arrayType][testType]; + message(" Average : " + times.mean() + "ms" + + " (Total: " + times.sum() + "ms)"); + message(" Min/Max : " + times.min() + "ms/" + + times.max() + "ms"); + message(" StdDev : " + times.stdDev() + "ms"); + } + } + + vmessage("array_chart.py JSON data:"); + chart_data = {'conf' : conf, 'stats' : { } }; + chart_data.stats[browser] = stats; + chart_data.stats['next_browser'] = {}; + vmessage(JSON.stringify(chart_data, null, 2)); + + $D('startButton').disabled = false; + $D('startButton').value = "Run Tests"; +} + +function run_next_iteration() { + var arrayType, testType, deltaTime; + + for (i = 0; i < conf.order_l2.length; i++) { + arrayType = conf.order_l2[i]; + if (arrayType === 'imageData' && (!do_imageData)) { + continue; + } + if (arrayType === 'arrayBuffer' && (!do_arrayBuffer)) { + continue; + } + for (j = 0; j < conf.order_l3.length; j++) { + testType = conf.order_l3[j]; + + deltaTime = testFunc[arrayType + "_" + testType](); + + stats[arrayType][testType].push(deltaTime); + vmessage("test " + (arrayType + "_" + testType) + + " time: " + (deltaTime) + "ms"); + } + } + + message("finished test iteration " + iteration); + if (iteration >= conf.iterations) { + setTimeout(finish, 1); + return; + } + iteration++; + setTimeout(run_next_iteration, 1); +} + +/* + * Test functions + */ + +testFunc["normal_create"] = function() { + var cnt, arrNormal, startTime, endTime; + vmessage("create normal array " + conf.create_cnt + "x, initialized to 0"); + + startTime = (new Date()).getTime(); + for (cnt = 0; cnt < conf.create_cnt; cnt++) { + arrNormal = new_normal(); + } + endTime = (new Date()).getTime(); + + return endTime - startTime; +}; + +testFunc["imageData_create"] = function() { + var cnt, arrImage, startTime, endTime; + vmessage("create imageData array " + conf.create_cnt + "x, initialized to 0"); + + startTime = (new Date()).getTime(); + for (cnt = 0; cnt < conf.create_cnt; cnt++) { + arrImage = new_imageData(); + } + endTime = (new Date()).getTime(); + + if (arrImage[103] !== 0) { + message("Initialization failed, arrImage[103] is: " + arrImage[103]); + throw("Initialization failed, arrImage[103] is: " + arrImage[103]); + } + return endTime - startTime; +}; + +testFunc["arrayBuffer_create"] = function() { + var cnt, arrBuffer, startTime, endTime; + vmessage("create arrayBuffer array " + conf.create_cnt + "x, initialized to 0"); + + startTime = (new Date()).getTime(); + for (cnt = 0; cnt < conf.create_cnt; cnt++) { + arrBuffer = new_arrayBuffer(); + } + endTime = (new Date()).getTime(); + + if (arrBuffer[103] !== 0) { + message("Initialization failed, arrBuffer[103] is: " + arrBuffer[103]); + throw("Initialization failed, arrBuffer[103] is: " + arrBuffer[103]); + } + return endTime - startTime; +}; + +function test_sequentialRead(arr) { + var i, j, cnt, startTime, endTime; + /* Initialize the array */ + copy_randlist(arr); + + startTime = (new Date()).getTime(); + i = 0; + j = 0; + for (cnt = 0; cnt < conf.read_cnt; cnt++) { + j = arr[i]; + i++; + if (i >= arraySize) { + i = 0; + } + } + endTime = (new Date()).getTime(); + + return endTime - startTime; +} + +function test_randomRead(arr) { + var i, cnt, startTime, endTime; + /* Initialize the array */ + copy_randlist(arr); // used as jumplist + + startTime = (new Date()).getTime(); + i = 0; + for (cnt = 0; cnt < conf.read_cnt; cnt++) { + i = (arr[i] + cnt) % arraySize; + } + endTime = (new Date()).getTime(); + + return endTime - startTime; +} + +function test_sequentialWrite(arr) { + var i, cnt, startTime, endTime; + /* Initialize the array */ + copy_randlist(arr); + + startTime = (new Date()).getTime(); + i = 0; + for (cnt = 0; cnt < conf.write_cnt; cnt++) { + arr[i] = (cnt % 256); + i++; + if (i >= arraySize) { + i = 0; + } + } + endTime = (new Date()).getTime(); + + return endTime - startTime; +} + +/* Sequential Read Tests */ +testFunc["normal_sequentialRead"] = function() { + vmessage("read normal array " + conf.read_cnt + "x"); + return test_sequentialRead(new_normal()); +}; + +testFunc["imageData_sequentialRead"] = function() { + vmessage("read imageData array " + conf.read_cnt + "x"); + return test_sequentialRead(new_imageData()); +}; + +testFunc["arrayBuffer_sequentialRead"] = function() { + vmessage("read arrayBuffer array " + conf.read_cnt + "x"); + return test_sequentialRead(new_arrayBuffer()); +}; + + +/* Random Read Tests */ +testFunc["normal_randomRead"] = function() { + vmessage("read normal array " + conf.read_cnt + "x"); + return test_randomRead(new_normal()); +}; + +testFunc["imageData_randomRead"] = function() { + vmessage("read imageData array " + conf.read_cnt + "x"); + return test_randomRead(new_imageData()); +}; + +testFunc["arrayBuffer_randomRead"] = function() { + vmessage("read arrayBuffer array " + conf.read_cnt + "x"); + return test_randomRead(new_arrayBuffer()); +}; + + +/* Sequential Write Tests */ +testFunc["normal_sequentialWrite"] = function() { + vmessage("write normal array " + conf.write_cnt + "x"); + return test_sequentialWrite(new_normal()); +}; + +testFunc["imageData_sequentialWrite"] = function() { + vmessage("write imageData array " + conf.write_cnt + "x"); + return test_sequentialWrite(new_imageData()); +}; + +testFunc["arrayBuffer_sequentialWrite"] = function() { + vmessage("write arrayBuffer array " + conf.write_cnt + "x"); + return test_sequentialWrite(new_arrayBuffer()); +}; + +init = function() { + vmessage(">> init"); + + $D('iterations').value = 10; + $D('arraySize').value = 10; + arraySize = parseInt($D('arraySize').value, 10) * 1024; + + message("Browser: " + browser); + + /* Determine browser binary array support */ + try { + ctx = $D('canvas').getContext('2d'); + new_imageData = new_imageData_createImageData; + new_imageData(); + do_imageData = true; + } catch (exc) { + vmessage("createImageData not supported: " + exc); + try { + ctx = $D('canvas').getContext('2d'); + new_imageData = new_imageData_getImageData; + blah = new_imageData(); + do_imageData = true; + } catch (exc) { + vmessage("getImageData not supported: " + exc); + } + } + if (! do_imageData) { + message("imageData arrays not supported"); + } + + try { + new_arrayBuffer(); + do_arrayBuffer = true; + } catch (exc) { + vmessage("Typed Arrays not supported: " + exc); + } + if (! do_arrayBuffer) { + message("Typed Arrays (ArrayBuffers) not suppoted"); + } + vmessage("<< init"); +} diff --git a/public/novnc/tests/assertions.js b/public/novnc/tests/assertions.js new file mode 100644 index 00000000..4bd0cf40 --- /dev/null +++ b/public/novnc/tests/assertions.js @@ -0,0 +1,98 @@ +// some useful assertions for noVNC +chai.use(function (_chai, utils) { + _chai.Assertion.addMethod('displayed', function (target_data) { + var obj = this._obj; + var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data; + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that + var data = new Uint8Array(data_cl); + var same = true; + var len = data_cl.length; + if (len != target_data.length) { + same = false; + } else { + for (var i = 0; i < len; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + } + if (!same) { + console.log("expected data: %o, actual data: %o", target_data, data); + } + this.assert(same, + "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", + "expected #{this} not to have displayed the image #{act}", + target_data, + data); + }); + + _chai.Assertion.addMethod('sent', function (target_data) { + var obj = this._obj; + obj.inspect = function () { + var res = { _websocket: obj._websocket, rQi: obj._rQi, _rQ: new Uint8Array(obj._rQ.buffer, 0, obj._rQlen), + _sQ: new Uint8Array(obj._sQ.buffer, 0, obj._sQlen) }; + res.prototype = obj; + return res; + }; + var data = obj._websocket._get_sent_data(); + var same = true; + for (var i = 0; i < obj.length; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + if (!same) { + console.log("expected data: %o, actual data: %o", target_data, data); + } + this.assert(same, + "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", + "expected #{this} not to have sent the data #{act}", + Array.prototype.slice.call(target_data), + Array.prototype.slice.call(data)); + }); + + _chai.Assertion.addProperty('array', function () { + utils.flag(this, 'array', true); + }); + + _chai.Assertion.overwriteMethod('equal', function (_super) { + return function assertArrayEqual(target) { + if (utils.flag(this, 'array')) { + var obj = this._obj; + + var i; + var same = true; + + if (utils.flag(this, 'deep')) { + for (i = 0; i < obj.length; i++) { + if (!utils.eql(obj[i], target[i])) { + same = false; + break; + } + } + + this.assert(same, + "expected #{this} to have elements deeply equal to #{exp}", + "expected #{this} not to have elements deeply equal to #{exp}", + Array.prototype.slice.call(target)); + } else { + for (i = 0; i < obj.length; i++) { + if (obj[i] != target[i]) { + same = false; + break; + } + } + + this.assert(same, + "expected #{this} to have elements equal to #{exp}", + "expected #{this} not to have elements equal to #{exp}", + Array.prototype.slice.call(target)); + } + } else { + _super.apply(this, arguments); + } + }; + }); +}); diff --git a/public/novnc/tests/base64.html b/public/novnc/tests/base64.html new file mode 100644 index 00000000..071dde63 --- /dev/null +++ b/public/novnc/tests/base64.html @@ -0,0 +1,91 @@ + + + + Native Base64 Tests + + + + + +

    Native Base64 Tests

    + +
    + Messages:
    + + +
    + + + diff --git a/public/novnc/tests/base64.js b/public/novnc/tests/base64.js new file mode 100644 index 00000000..6ade00a3 --- /dev/null +++ b/public/novnc/tests/base64.js @@ -0,0 +1,12 @@ +// The following results in 'hello [MANGLED]' +// +// Filed as https://github.com/ry/node/issues/issue/402 + +var sys = require("sys"), + buf = new Buffer(1024), len, + str1 = "aGVsbG8g", // 'hello ' + str2 = "d29ybGQ=", // 'world' + +len = buf.write(str1, 0, 'base64'); +len += buf.write(str2, len, 'base64'); +sys.log("decoded result: " + buf.toString('binary', 0, len)); diff --git a/public/novnc/tests/browser.js b/public/novnc/tests/browser.js new file mode 100644 index 00000000..7cf8279c --- /dev/null +++ b/public/novnc/tests/browser.js @@ -0,0 +1,134 @@ +/* + * From: + * http://www.quirksmode.org/js/detect.html + */ + +var Browser = { + init: function () { + this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; + this.version = this.searchVersion(navigator.userAgent) + || this.searchVersion(navigator.appVersion) + || "an unknown version"; + this.majorVersion = this.searchMajorVersion(navigator.userAgent) + || this.searchMajorVersion(navigator.appVersion) + || "an unknown version"; + this.fullVersion = this.searchFullVersion(navigator.userAgent) + || this.searchFullVersion(navigator.appVersion) + || "an unknown version"; + this.OS = this.searchString(this.dataOS) || "an unknown OS"; + }, + searchString: function (data) { + for (var i=0;i + + + Canvas Performance Test + + + + + + + + + Iterations:   + + Width:   + Height:   + +   + +

    + + Canvas (should see three squares and two happy faces):
    + + Canvas not supported. + + +
    + Results:
    + + + + + diff --git a/public/novnc/tests/cursor.html b/public/novnc/tests/cursor.html new file mode 100644 index 00000000..fd25e4bb --- /dev/null +++ b/public/novnc/tests/cursor.html @@ -0,0 +1,135 @@ + + + + Cursor Change test + + + + + + + + +

    Roll over the buttons to test cursors

    +
    + + + +
    +
    +
    + Debug:
    + +
    +
    + + Canvas not supported. + + + + + diff --git a/public/novnc/tests/face.png b/public/novnc/tests/face.png new file mode 100644 index 0000000000000000000000000000000000000000..74c30d82f9736beffef5db259b3e40bb6de4ea2b GIT binary patch literal 2303 zcmVDcqz&LP?>7h(O|oIKntKx%ray+TL^a+~)T0!`f>n3cTgh|6ym( z?DyOM%r~>k`rm(L+4(DM%f`8sHkw5f14ij{imA#v*WJ|Q00hwIJr+{BoC65U{pI4W zU}}#{ib*uVz}!qTPqpNmuB>m}lqp;|fAU$yL}^e;Q60pI*t4&1c<9(`|Ju24|G~27 zSu((R_><5N$zql^I-kpZ zxo72fZ~prFbr*Ln>Byv0XOf=!`#-n6^y-54_LC<^95UH_mW+wwC=H*dHC zfI|R*;zVqF>9sA7|5}LTvB!Vsx+y6{6er9w%vO%8vxXRB+;yMd{=%EPb`#NtYp-%s z?wYlidY&JIfvN;VOc`SgrP8T$t8I>iF=`wHvRWC>A@;>qrt&O#}eAoZo%xO_yA}?zTJc5judC%A~baBA-Me zF2r#V1p#RlRD2k9LH8npY{z6l-EsE? zJ*)M-x6P7>0DNonSO2(bMgPIR6|25f_R6QmN2aGrNf?A-XpAvhDJ3hF%3q(|N=QJW z_u;#5z5Rj4=2kbACZn|079&FpaGIK$8|v#zr4qC3d++#8V|^Y(EGZ325m!4aPE3C9 zj=K(ha&*DM#TTvYUcF|0ds|DPPzQin$YwKNxpYlyYrSP@P;68%Q5xC1_e0;CG$w=z zeZMqO9No8n;=O(T)O5J8z46b#-}2SV*VfJxYUio7rST8Frsuldjw8V@1i%;r7+?Wx z_q%&;zU7`rANlE~4QoO2-SaJK*B)rQ8VEl)fRXpq|e(t8a}ZCjpv8Kwdg z0TN(f7QjfIOy?Fe%S4sInz=LoitxsFwRe9T`Xpe1B9KFjgE4@EIS2M1jKXkyYy>cX zIM6o`g~|BnF@Vi}>FNS71{nESOULr^*c<8Y^XGc1DVW+**Senc7*PZh*4hFTNROO6 zas2o(Arg?BHx#p;n?9JD$IWhb?Ac-PSa{b1Ojt@Km6MzDcs>TG+WCTbODIzo9*fB;+kyHn6x>3ss zn^~X)WX~qk097`dv7-bOgOX|yC4ijWPGv|KBLG*4Y3>$k=BM>!r&?7sYNu^b1;msH zoK!PNf$`aBnHf_`KoKY=2#^2?K!Bu@0VpC9gXL8+<)+)_?Gph+)yfqv^NM?6)f2@q zQQyv9Ehj;h+44dD>~aO5h$yP^s?BK?^u4pon9%5mIh}rKI9GpF&D@&*b6eN4Vf@HW zW;KE$qA-(f3MLQ3RA4+9f2PQXslWte0#F1~CB>k`pDLzvd81=vLW?h(939HHY&;|7 zGsQE@lnmp_0JG{rLC30%gZrNIihVP%8AgSuLKJ|i>7QxKE1eiRxPMW1kI@xopO;id zjIw!VNzI&v
    wW2S7wZO0V?3EN@$R^}`hJft4#4Cx#AecQbX33s>d}?WtU&a>2k+SxsGkmkfP8BAe9nAB#BI-(csv9J=Z>v&Nw6wgXI`s2IqWc zH-qJ2)iG8Fs{k07X);qJPpI(UGI&$P!&dq-sTx21;MM$jTT_{ZsT6lzmpcvs7LLu4 zq>zb_Qp(8xSe1UW>gxM5xh#nfK^m}O6`0)$mIcd!fs9Y8q>}+HhO{mlo!Hq;B5^+b zV29h?l8i7W zHYPGUG&*E_xt(4&J$Crmq5n87zepEWyDqO%T$fvxHA@QoY7mvyQb-}CluAe$hbMID zx9MD}YxPZ;x&=laC3Tc^*_dfE5^xA<<}7A>N$eMo_U}$Y!&`b&*+nkroI5V(+~w6x z0>FZ?{wVNjsjtGMQc^0VlvFAyzhnHrrBd0}&Q2hq_c5KJCYp`m#=aUXamD5ObJf(~_N^7l+Hku681c23KG260j+j4B%vMuhM$(Xxg z&;I|PG0r(STq0xWG!h5^!)YYTVl}{74V}+@QX_)d7PBquiza;mp++)VGluGdoI5eb Z{tpYglKBOBdFlWF002ovPDHLkV1m%%QeprA literal 0 HcmV?d00001 diff --git a/public/novnc/tests/face.png.js b/public/novnc/tests/face.png.js new file mode 100644 index 00000000..e0b5d6ce --- /dev/null +++ b/public/novnc/tests/face.png.js @@ -0,0 +1 @@ +var face64 = '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'; diff --git a/public/novnc/tests/fake.websocket.js b/public/novnc/tests/fake.websocket.js new file mode 100644 index 00000000..21012059 --- /dev/null +++ b/public/novnc/tests/fake.websocket.js @@ -0,0 +1,91 @@ +var FakeWebSocket; + +(function () { + // PhantomJS can't create Event objects directly, so we need to use this + function make_event(name, props) { + var evt = document.createEvent('Event'); + evt.initEvent(name, true, true); + if (props) { + for (var prop in props) { + evt[prop] = props[prop]; + } + } + return evt; + } + + FakeWebSocket = function (uri, protocols) { + this.url = uri; + this.binaryType = "arraybuffer"; + this.extensions = ""; + + if (!protocols || typeof protocols === 'string') { + this.protocol = protocols; + } else { + this.protocol = protocols[0]; + } + + this._send_queue = new Uint8Array(20000); + + this.readyState = FakeWebSocket.CONNECTING; + this.bufferedAmount = 0; + + this.__is_fake = true; + }; + + FakeWebSocket.prototype = { + close: function (code, reason) { + this.readyState = FakeWebSocket.CLOSED; + if (this.onclose) { + this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true })); + } + }, + + send: function (data) { + if (this.protocol == 'base64') { + data = Base64.decode(data); + } else { + data = new Uint8Array(data); + } + this._send_queue.set(data, this.bufferedAmount); + this.bufferedAmount += data.length; + }, + + _get_sent_data: function () { + var res = new Uint8Array(this._send_queue.buffer, 0, this.bufferedAmount); + this.bufferedAmount = 0; + return res; + }, + + _open: function (data) { + this.readyState = FakeWebSocket.OPEN; + if (this.onopen) { + this.onopen(make_event('open')); + } + }, + + _receive_data: function (data) { + this.onmessage(make_event("message", { 'data': data })); + } + }; + + FakeWebSocket.OPEN = WebSocket.OPEN; + FakeWebSocket.CONNECTING = WebSocket.CONNECTING; + FakeWebSocket.CLOSING = WebSocket.CLOSING; + FakeWebSocket.CLOSED = WebSocket.CLOSED; + + FakeWebSocket.__is_fake = true; + + FakeWebSocket.replace = function () { + if (!WebSocket.__is_fake) { + var real_version = WebSocket; + WebSocket = FakeWebSocket; + FakeWebSocket.__real_version = real_version; + } + }; + + FakeWebSocket.restore = function () { + if (WebSocket.__is_fake) { + WebSocket = WebSocket.__real_version; + } + }; +})(); diff --git a/public/novnc/tests/input.html b/public/novnc/tests/input.html new file mode 100644 index 00000000..04d7edef --- /dev/null +++ b/public/novnc/tests/input.html @@ -0,0 +1,132 @@ + + + Input Test + +

    + + Canvas: + +
    + + Canvas not supported. + + +
    + Results:
    + + + + + + + + + + + + + + diff --git a/public/novnc/tests/keyboard-tests.html b/public/novnc/tests/keyboard-tests.html new file mode 100644 index 00000000..a30aa6e0 --- /dev/null +++ b/public/novnc/tests/keyboard-tests.html @@ -0,0 +1,29 @@ + + + + + Mocha Tests + + + + +
    + + + + + + + + + + diff --git a/public/novnc/tests/run_from_console.casper.js b/public/novnc/tests/run_from_console.casper.js new file mode 100644 index 00000000..6a738a3e --- /dev/null +++ b/public/novnc/tests/run_from_console.casper.js @@ -0,0 +1,114 @@ +var Spooky = require('spooky'); +var path = require('path'); + +var phantom_path = require('phantomjs').path; +var casper_path = path.resolve(__dirname, '../node_modules/casperjs/bin/casperjs'); +process.env.PHANTOMJS_EXECUTABLE = phantom_path; +var casper_opts = { + child: { + transport: 'http', + command: casper_path + }, + casper: { + logLevel: 'debug', + verbose: true + } +}; + +var provide_emitter = function(file_paths, debug_port) { + if (debug_port) { + casper_opts.child['remote-debugger-port'] = debug_port; + var debug_url = ('https://localhost:' + debug_port + + '/webkit/inspector/inspector.html?page='); + console.info('[remote-debugger] Navigate to ' + debug_url + '1 and ' + + 'run `__run();` in the console to continue loading.' + + '\n[remote-debugger] Navigate to ' + debug_url + '2 to ' + + 'view the actual page source.\n' + + '[remote-debugger] Use the `debugger;` statement to ' + + 'trigger an initial breakpoint.'); + } + + var spooky = new Spooky(casper_opts, function(err) { + if (err) { + if (err.stack) console.warn(err.stack); + else console.warn(err); + return; + } + spooky.start('about:blank'); + + file_paths.forEach(function(file_path, path_ind) { + spooky.thenOpen('file://'+file_path); + spooky.waitFor(function() { + return this.getGlobal('__mocha_done') === true; + }, + [{ path_ind: path_ind }, function() { + var res_json = { + file_ind: path_ind + }; + + res_json.num_tests = this.evaluate(function() { return document.querySelectorAll('li.test').length; }); + res_json.num_passes = this.evaluate(function() { return document.querySelectorAll('li.test.pass').length; }); + res_json.num_fails = this.evaluate(function() { return document.querySelectorAll('li.test.fail').length; }); + res_json.num_slow = this.evaluate(function() { return document.querySelectorAll('li.test.pass:not(.fast):not(.pending)').length; }); + res_json.num_skipped = this.evaluate(function () { return document.querySelectorAll('li.test.pending').length; }); + res_json.duration = this.evaluate(function() { return document.querySelector('li.duration em').textContent; }); + + res_json.suites = this.evaluate(function() { + var traverse_node = function(elem) { + var res; + if (elem.classList.contains('suite')) { + res = { + type: 'suite', + name: elem.querySelector('h1').textContent, + has_subfailures: elem.querySelectorAll('li.test.fail').length > 0, + }; + + var child_elems = elem.querySelector('ul').children; + res.children = Array.prototype.map.call(child_elems, traverse_node); + return res; + } + else { + var h2_content = elem.querySelector('h2').childNodes; + res = { + type: 'test', + text: h2_content[0].textContent, + }; + + if (elem.classList.contains('pass')) { + res.pass = true; + if (elem.classList.contains('pending')) { + res.slow = false; + res.skipped = true; + } + else { + res.slow = !elem.classList.contains('fast'); + res.skipped = false; + res.duration = h2_content[1].textContent; + } + } + else { + res.error = elem.querySelector('pre.error').textContent; + } + + return res; + } + }; + var top_suites = document.querySelectorAll('#mocha-report > li.suite'); + return Array.prototype.map.call(top_suites, traverse_node); + }); + + res_json.replay = this.evaluate(function() { return document.querySelector('a.replay').textContent; }); + + this.emit('test_ready', res_json); + }]); + }); + spooky.run(); + }); + + return spooky; +}; + +module.exports = { + provide_emitter: provide_emitter, + name: 'SpookyJS (CapserJS on PhantomJS)' +}; diff --git a/public/novnc/tests/run_from_console.js b/public/novnc/tests/run_from_console.js new file mode 100644 index 00000000..371e861a --- /dev/null +++ b/public/novnc/tests/run_from_console.js @@ -0,0 +1,361 @@ +#!/usr/bin/env node +var ansi = require('ansi'); +var program = require('commander'); +var path = require('path'); +var fs = require('fs'); + +var make_list = function(val) { + return val.split(','); +}; + +program + .option('-t, --tests ', 'Run the specified html-file-based test(s). \'testlist\' should be a comma-separated list', make_list, []) + .option('-a, --print-all', 'Print all tests, not just the failures') + .option('--disable-color', 'Explicitly disable color') + .option('-c, --color', 'Explicitly enable color (default is to use color when not outputting to a pipe)') + .option('-i, --auto-inject ', 'Treat the test list as a set of mocha JS files, and automatically generate HTML files with which to test test. \'includefiles\' should be a comma-separated list of paths to javascript files to include in each of the generated HTML files', make_list, null) + .option('-p, --provider ', 'Use the given provider (defaults to "casper"). Currently, may be "casper" or "zombie"', 'casper') + .option('-g, --generate-html', 'Instead of running the tests, just return the path to the generated HTML file, then wait for user interaction to exit (should be used with .js tests).') + .option('-o, --open-in-browser', 'Open the generated HTML files in a web browser using the "open" module (must be used with the "-g"/"--generate-html" option).') + .option('--output-html', 'Instead of running the tests, just output the generated HTML source to STDOUT (should be used with .js tests)') + .option('-d, --debug', 'Show debug output (the "console" event) from the provider') + .option('-r, --relative', 'Use relative paths in the generated HTML file') + .option('--debugger ', 'Enable the remote debugger for CasperJS') + .parse(process.argv); + +if (program.tests.length === 0) { + program.tests = fs.readdirSync(__dirname).filter(function(f) { return (/^test\.(\w|\.|-)+\.js$/).test(f); }); + program.tests = program.tests.map(function (f) { return path.resolve(__dirname, f); }); // add full paths in + console.log('using files %s', program.tests); +} + +var file_paths = []; + +var all_js = program.tests.reduce(function(a,e) { return a && e.slice(-3) == '.js'; }, true); + +var get_path = function (/* arguments */) { + if (program.relative) { + return path.join.apply(null, arguments); + } else { + var args = Array.prototype.slice.call(arguments); + args.unshift(__dirname, '..'); + return path.resolve.apply(null, args); + } +}; + +var get_path_cwd = function (/* arguments */) { + if (program.relative) { + var part_path = path.join.apply(null, arguments); + return path.relative(path.join(__dirname, '..'), path.resolve(process.cwd(), part_path)); + } else { + var args = Array.prototype.slice.call(arguments); + args.unshift(process.cwd()); + return path.resolve.apply(null, args); + } +}; + +if (all_js && !program.autoInject) { + var all_modules = {}; + + // uses the first instance of the string 'requires local modules: ' + program.tests.forEach(function (testname) { + var full_path = path.resolve(process.cwd(), testname); + var content = fs.readFileSync(full_path).toString(); + var ind = content.indexOf('requires local modules: '); + if (ind > -1) { + ind += 'requires local modules: '.length; + var eol = content.indexOf('\n', ind); + var modules = content.slice(ind, eol).split(/,\s*/); + modules.forEach(function (mod) { + all_modules[get_path('include/', mod) + '.js'] = 1; + }); + } + + var fakes_ind = content.indexOf('requires test modules: '); + if (fakes_ind > -1) { + fakes_ind += 'requires test modules: '.length; + var fakes_eol = content.indexOf('\n', fakes_ind); + var fakes_modules = content.slice(fakes_ind, fakes_eol).split(/,\s*/); + fakes_modules.forEach(function (mod) { + all_modules[get_path('tests/', mod) + '.js'] = 1; + }); + } + }); + + program.autoInject = Object.keys(all_modules); +} + +if (program.autoInject) { + var temp = require('temp'); + temp.track(); + + var template = { + header: "\n\n\n\n\n
    ", + script_tag: function(p) { return ""; }, + footer: "\n\n" + }; + + template.header += "\n" + template.script_tag(get_path('node_modules/chai/chai.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/mocha/mocha.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/sinon/pkg/sinon.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/sinon-chai/lib/sinon-chai.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/sinon-chai/lib/sinon-chai.js')); + template.header += "\n"; + + + template.header = program.autoInject.reduce(function(acc, sn) { + return acc + "\n" + template.script_tag(get_path_cwd(sn)); + }, template.header); + + file_paths = program.tests.map(function(jsn, ind) { + var templ = template.header; + templ += "\n"; + templ += template.script_tag(get_path_cwd(jsn)); + templ += template.footer; + + var tempfile = temp.openSync({ prefix: 'novnc-zombie-inject-', suffix: '-file_num-'+ind+'.html' }); + fs.writeSync(tempfile.fd, templ); + fs.closeSync(tempfile.fd); + return tempfile.path; + }); + +} +else { + file_paths = program.tests.map(function(fn) { + return path.resolve(process.cwd(), fn); + }); +} + +var use_ansi = false; +if (program.color) use_ansi = true; +else if (program.disableColor) use_ansi = false; +else if (process.stdout.isTTY) use_ansi = true; + +var cursor = ansi(process.stdout, { enabled: use_ansi }); + +if (program.outputHtml) { + file_paths.forEach(function(path, path_ind) { + fs.readFile(path, function(err, data) { + if (err) { + console.warn(error.stack); + return; + } + + if (use_ansi) { + cursor + .bold() + .write(program.tests[path_ind]) + .reset() + .write("\n") + .write(Array(program.tests[path_ind].length+1).join('=')) + .write("\n\n"); + } + + cursor + .write(data) + .write("\n\n"); + }); + }); +} + +if (program.generateHtml) { + var open_browser; + if (program.openInBrowser) { + open_browser = require('open'); + } + + file_paths.forEach(function(path, path_ind) { + cursor + .bold() + .write(program.tests[path_ind]) + .write(": ") + .reset() + .write(path) + .write("\n"); + + if (program.openInBrowser) { + open_browser(path); + } + }); + console.log(''); +} + +if (program.generateHtml) { + process.stdin.resume(); // pause until C-c + process.on('SIGINT', function() { + process.stdin.pause(); // exit + }); +} + +if (!program.outputHtml && !program.generateHtml) { + var failure_count = 0; + + var prov = require(path.resolve(__dirname, 'run_from_console.'+program.provider+'.js')); + + cursor + .write("Running tests ") + .bold() + .write(program.tests.join(', ')) + .reset() + .grey() + .write(' using provider '+prov.name) + .reset() + .write("\n"); + //console.log("Running tests %s using provider %s", program.tests.join(', '), prov.name); + + var provider = prov.provide_emitter(file_paths, program.debugger); + provider.on('test_ready', function(test_json) { + console.log(''); + + filename = program.tests[test_json.file_ind]; + + cursor.bold(); + console.log('Results for %s:', filename); + console.log(Array('Results for :'.length+filename.length+1).join('=')); + cursor.reset(); + + console.log(''); + + cursor + .write(''+test_json.num_tests+' tests run, ') + .green() + .write(''+test_json.num_passes+' passed'); + if (test_json.num_slow > 0) { + cursor + .reset() + .write(' ('); + cursor + .yellow() + .write(''+test_json.num_slow+' slow') + .reset() + .write(')'); + } + cursor + .reset() + .write(', '); + cursor + .red() + .write(''+test_json.num_fails+' failed'); + if (test_json.num_skipped > 0) { + cursor + .reset() + .write(', ') + .grey() + .write(''+test_json.num_skipped+' skipped'); + } + cursor + .reset() + .write(' -- duration: '+test_json.duration+"s\n"); + + console.log(''); + + if (test_json.num_fails > 0 || program.printAll) { + var extract_error_lines = function (err) { + // the split is to avoid a weird thing where in PhantomJS where we get a stack trace too + var err_lines = err.split('\n'); + if (err_lines.length == 1) { + return err_lines[0]; + } else { + var ind; + for (ind = 0; ind < err_lines.length; ind++) { + var at_ind = err_lines[ind].trim().indexOf('at '); + if (at_ind === 0) { + break; + } + } + + return err_lines.slice(0, ind).join('\n'); + } + }; + + var traverse_tree = function(indentation, node) { + if (node.type == 'suite') { + if (!node.has_subfailures && !program.printAll) return; + + if (indentation === 0) { + cursor.bold(); + console.log(node.name); + console.log(Array(node.name.length+1).join('-')); + cursor.reset(); + } + else { + cursor + .write(Array(indentation+3).join('#')) + .bold() + .write(' '+node.name+' ') + .reset() + .write(Array(indentation+3).join('#')) + .write("\n"); + } + + console.log(''); + + for (var i = 0; i < node.children.length; i++) { + traverse_tree(indentation+1, node.children[i]); + } + } + else { + if (!node.pass) { + cursor.magenta(); + console.log('- failed: '+node.text+test_json.replay); + cursor.red(); + console.log(' '+extract_error_lines(node.error)); + cursor.reset(); + console.log(''); + } + else if (program.printAll) { + if (node.skipped) { + cursor + .grey() + .write('- skipped: '+node.text); + } + else { + if (node.slow) cursor.yellow(); + else cursor.green(); + + cursor + .write('- pass: '+node.text) + .grey() + .write(' ('+node.duration+') '); + } + /*if (node.slow) cursor.yellow(); + else cursor.green();*/ + cursor + //.write(test_json.replay) + .reset() + .write("\n"); + console.log(''); + } + } + }; + + for (var i = 0; i < test_json.suites.length; i++) { + traverse_tree(0, test_json.suites[i]); + } + } + + if (test_json.num_fails === 0) { + cursor.fg.green(); + console.log('all tests passed :-)'); + cursor.reset(); + } + }); + + if (program.debug) { + provider.on('console', function(line) { + // log to stderr + console.error(line); + }); + } + + provider.on('error', function(line) { + // log to stderr + console.error('ERROR: ' + line); + }); + + /*gprom.finally(function(ph) { + ph.exit(); + // exit with a status code that actually gives information + if (program.exitWithFailureCount) process.exit(failure_count); + });*/ +} diff --git a/public/novnc/tests/run_from_console.zombie.js b/public/novnc/tests/run_from_console.zombie.js new file mode 100644 index 00000000..7280f75d --- /dev/null +++ b/public/novnc/tests/run_from_console.zombie.js @@ -0,0 +1,82 @@ +var Browser = require('zombie'); +var path = require('path'); +var EventEmitter = require('events').EventEmitter; +var Q = require('q'); + +var provide_emitter = function(file_paths) { + var emitter = new EventEmitter(); + + file_paths.reduce(function(prom, file_path, path_ind) { + return prom.then(function(browser) { + browser.visit('file://'+file_path, function() { + if (browser.error) throw new Error(browser.errors); + + var res_json = {}; + res_json.file_ind = path_ind; + + res_json.num_tests = browser.querySelectorAll('li.test').length; + res_json.num_fails = browser.querySelectorAll('li.test.fail').length; + res_json.num_passes = browser.querySelectorAll('li.test.pass').length; + res_json.num_slow = browser.querySelectorAll('li.test.pass:not(.fast)').length; + res_json.num_skipped = browser.querySelectorAll('li.test.pending').length; + res_json.duration = browser.text('li.duration em'); + + var traverse_node = function(elem) { + var classList = elem.className.split(' '); + var res; + if (classList.indexOf('suite') > -1) { + res = { + type: 'suite', + name: elem.querySelector('h1').textContent, + has_subfailures: elem.querySelectorAll('li.test.fail').length > 0 + }; + + var child_elems = elem.querySelector('ul').children; + res.children = Array.prototype.map.call(child_elems, traverse_node); + return res; + } + else { + var h2_content = elem.querySelector('h2').childNodes; + res = { + type: 'test', + text: h2_content[0].textContent + }; + + if (classList.indexOf('pass') > -1) { + res.pass = true; + if (classList.indexOf('pending') > -1) { + res.slow = false; + res.skipped = true; + } + else { + res.slow = classList.indexOf('fast') < 0; + res.skipped = false; + res.duration = h2_content[1].textContent; + } + } + else { + res.error = elem.querySelector('pre.error').textContent; + } + + return res; + } + }; + + var top_suites = browser.querySelectorAll('#mocha-report > li.suite'); + res_json.suites = Array.prototype.map.call(top_suites, traverse_node); + res_json.replay = browser.querySelector('a.replay').textContent; + + emitter.emit('test_ready', res_json); + }); + + return new Browser(); + }); + }, Q(new Browser())); + + return emitter; +}; + +module.exports = { + provide_emitter: provide_emitter, + name: 'ZombieJS' +}; diff --git a/public/novnc/tests/stats.js b/public/novnc/tests/stats.js new file mode 100644 index 00000000..cd3011cb --- /dev/null +++ b/public/novnc/tests/stats.js @@ -0,0 +1,53 @@ +/* + * Define some useful statistical functions on arrays of numbers + */ + +Array.prototype.sum = function() { + var i, sum = 0; + for (i = 0; i < this.length; i++) { + sum += this[i]; + } + return sum; +} + +Array.prototype.max = function() { + return Math.max.apply(null, this); +} + +Array.prototype.min = function() { + return Math.min.apply(null, this); +} + +Array.prototype.mean = function() { + return this.sum() / this.length; +} +Array.prototype.average = Array.prototype.mean; + +Array.prototype.median = function() { + var sorted = this.sort( function(a,b) { return a-b; }), + len = sorted.length; + if (len % 2) { + return sorted[Math.floor(len / 2)]; // Odd + } else { + return (sorted[len/2 - 1] + sorted[len/2]) / 2; // Even + } +} + +Array.prototype.stdDev = function(sample) { + var i, sumSqr = 0, mean = this.mean(), N; + + if (sample) { + // Population correction if this is a sample + N = this.length - 1; + } else { + // Standard deviation of just the array + N = this.length; + } + + for (i = 0; i < this.length; i++) { + sumSqr += Math.pow(this[i] - mean, 2); + } + + return Math.sqrt(sumSqr / N); +} + diff --git a/public/novnc/tests/test.base64.js b/public/novnc/tests/test.base64.js new file mode 100644 index 00000000..b2646a0f --- /dev/null +++ b/public/novnc/tests/test.base64.js @@ -0,0 +1,33 @@ +// requires local modules: base64 +var assert = chai.assert; +var expect = chai.expect; + +describe('Base64 Tools', function() { + "use strict"; + + var BIN_ARR = new Array(256); + for (var i = 0; i < 256; i++) { + BIN_ARR[i] = i; + } + + var B64_STR = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=="; + + + describe('encode', function() { + it('should encode a binary string into Base64', function() { + var encoded = Base64.encode(BIN_ARR); + expect(encoded).to.equal(B64_STR); + }); + }); + + describe('decode', function() { + it('should decode a Base64 string into a normal string', function() { + var decoded = Base64.decode(B64_STR); + expect(decoded).to.deep.equal(BIN_ARR); + }); + + it('should throw an error if we have extra characters at the end of the string', function() { + expect(function () { Base64.decode(B64_STR+'abcdef'); }).to.throw(Error); + }); + }); +}); diff --git a/public/novnc/tests/test.display.js b/public/novnc/tests/test.display.js new file mode 100644 index 00000000..32a92e22 --- /dev/null +++ b/public/novnc/tests/test.display.js @@ -0,0 +1,461 @@ +// requires local modules: util, base64, display +// requires test modules: assertions +/* jshint expr: true */ +var expect = chai.expect; + +describe('Display/Canvas Helper', function () { + var checked_data = [ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]; + checked_data = new Uint8Array(checked_data); + + var basic_data = [0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]; + basic_data = new Uint8Array(basic_data); + + function make_image_canvas (input_data) { + var canvas = document.createElement('canvas'); + canvas.width = 4; + canvas.height = 4; + var ctx = canvas.getContext('2d'); + var data = ctx.createImageData(4, 4); + for (var i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; } + ctx.putImageData(data, 0, 0); + return canvas; + } + + describe('checking for cursor uri support', function () { + beforeEach(function () { + this._old_browser_supports_cursor_uris = Util.browserSupportsCursorURIs; + }); + + it('should disable cursor URIs if there is no support', function () { + Util.browserSupportsCursorURIs = function () { return false; }; + var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false }); + expect(display._cursor_uri).to.be.false; + }); + + it('should enable cursor URIs if there is support', function () { + Util.browserSupportsCursorURIs = function () { return true; }; + var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false }); + expect(display._cursor_uri).to.be.true; + }); + + it('respect the cursor_uri option if there is support', function () { + Util.browserSupportsCursorURIs = function () { return false; }; + var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false, cursor_uri: false }); + expect(display._cursor_uri).to.be.false; + }); + + afterEach(function () { + Util.browserSupportsCursorURIs = this._old_browser_supports_cursor_uris; + }); + }); + + describe('viewport handling', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(5, 5); + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); + display.getCleanDirtyReset(); + }); + + it('should take viewport location into consideration when drawing images', function () { + display.set_width(4); + display.set_height(4); + display.viewportChangeSize(2, 2); + display.drawImage(make_image_canvas(basic_data), 1, 1); + + var expected = new Uint8Array(16); + var i; + for (i = 0; i < 8; i++) { expected[i] = basic_data[i]; } + for (i = 8; i < 16; i++) { expected[i] = 0; } + expect(display).to.have.displayed(expected); + }); + + it('should redraw the left side when shifted left', function () { + display.viewportChangePos(-1, 0); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 2, h: 3 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 1, w: 2, h: 3 }); + }); + + it('should redraw the right side when shifted right', function () { + display.viewportChangePos(1, 0); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 2, y: 1, w: 2, h: 3 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 4, y: 1, w: 1, h: 3 }); + }); + + it('should redraw the top part when shifted up', function () { + display.viewportChangePos(0, -1); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 3, h: 2 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 0, w: 3, h: 1 }); + }); + + it('should redraw the bottom part when shifted down', function () { + display.viewportChangePos(0, 1); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 2 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 4, w: 3, h: 1 }); + }); + + it('should reset the entire viewport to being clean after calculating the clean/dirty boxes', function () { + display.viewportChangePos(0, 1); + var cdr1 = display.getCleanDirtyReset(); + var cdr2 = display.getCleanDirtyReset(); + expect(cdr1).to.not.deep.equal(cdr2); + expect(cdr2.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 3 }); + expect(cdr2.dirtyBoxes).to.be.empty; + }); + + it('should simply mark the whole display area as dirty if not using viewports', function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: false }); + display.resize(5, 5); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 0, y: 0, w: 0, h: 0 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 0, w: 5, h: 5 }); + }); + }); + + describe('clipping', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + }); + + it('should report true when no max-size and framebuffer > viewport', function () { + display.viewportChangeSize(2,2); + var clipping = display.clippingDisplay(); + expect(clipping).to.be.true; + }); + + it('should report false when no max-size and framebuffer = viewport', function () { + var clipping = display.clippingDisplay(); + expect(clipping).to.be.false; + }); + + it('should report true when viewport > max-size and framebuffer > viewport', function () { + display.viewportChangeSize(2,2); + display.set_maxWidth(1); + display.set_maxHeight(2); + var clipping = display.clippingDisplay(); + expect(clipping).to.be.true; + }); + + it('should report true when viewport > max-size and framebuffer = viewport', function () { + display.set_maxWidth(1); + display.set_maxHeight(2); + var clipping = display.clippingDisplay(); + expect(clipping).to.be.true; + }); + }); + + describe('resizing', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + }); + + it('should change the size of the logical canvas', function () { + display.resize(5, 7); + expect(display._fb_width).to.equal(5); + expect(display._fb_height).to.equal(7); + }); + + it('should update the viewport dimensions', function () { + sinon.spy(display, 'viewportChangeSize'); + display.resize(2, 2); + expect(display.viewportChangeSize).to.have.been.calledOnce; + }); + }); + + describe('rescaling', function () { + var display; + var canvas; + + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + canvas = display.get_target(); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should not change the bitmap size of the canvas', function () { + display.set_scale(0.5); + expect(canvas.width).to.equal(4); + expect(canvas.height).to.equal(3); + }); + + it('should change the effective rendered size of the canvas', function () { + display.set_scale(0.5); + expect(canvas.clientWidth).to.equal(2); + expect(canvas.clientHeight).to.equal(2); + }); + }); + + describe('autoscaling', function () { + var display; + var canvas; + + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + canvas = display.get_target(); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should preserve aspect ratio while autoscaling', function () { + display.autoscale(16, 9); + expect(canvas.clientWidth / canvas.clientHeight).to.equal(4 / 3); + }); + + it('should use width to determine scale when the current aspect ratio is wider than the target', function () { + expect(display.autoscale(9, 16)).to.equal(9 / 4); + expect(canvas.clientWidth).to.equal(9); + expect(canvas.clientHeight).to.equal(7); // round 9 / (4 / 3) + }); + + it('should use height to determine scale when the current aspect ratio is taller than the target', function () { + expect(display.autoscale(16, 9)).to.equal(3); // 9 / 3 + expect(canvas.clientWidth).to.equal(12); // 16 * (4 / 3) + expect(canvas.clientHeight).to.equal(9); + + }); + + it('should not change the bitmap size of the canvas', function () { + display.autoscale(16, 9); + expect(canvas.width).to.equal(4); + expect(canvas.height).to.equal(3); + }); + + it('should not upscale when downscaleOnly is true', function () { + expect(display.autoscale(2, 2, true)).to.equal(0.5); + expect(canvas.clientWidth).to.equal(2); + expect(canvas.clientHeight).to.equal(2); + + expect(display.autoscale(16, 9, true)).to.equal(1.0); + expect(canvas.clientWidth).to.equal(4); + expect(canvas.clientHeight).to.equal(3); + }); + }); + + describe('drawing', function () { + + // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the + // basic cases + function drawing_tests (pref_js) { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: pref_js }); + display.resize(4, 4); + }); + + it('should clear the screen on #clear without a logo set', function () { + display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]); + display._logo = null; + display.clear(); + display.resize(4, 4); + var empty = []; + for (var i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; } + expect(display).to.have.displayed(new Uint8Array(empty)); + }); + + it('should draw the logo on #clear with a logo set', function (done) { + display._logo = { width: 4, height: 4, data: make_image_canvas(checked_data).toDataURL() }; + display._drawCtx._act_drawImg = display._drawCtx.drawImage; + display._drawCtx.drawImage = function (img, x, y) { + this._act_drawImg(img, x, y); + expect(display).to.have.displayed(checked_data); + done(); + }; + display.clear(); + expect(display._fb_width).to.equal(4); + expect(display._fb_height).to.equal(4); + }); + + it('should support filling a rectangle with particular color via #fillRect', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0]); + display.fillRect(2, 2, 2, 2, [0xff, 0, 0]); + expect(display).to.have.displayed(checked_data); + }); + + it('should support copying an portion of the canvas via #copyImage', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]); + display.copyImage(0, 0, 2, 2, 2, 2); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing tile data with a background color and sub tiles', function () { + display.startTile(0, 0, 4, 4, [0, 0xff, 0]); + display.subTile(0, 0, 2, 2, [0xff, 0, 0]); + display.subTile(2, 2, 2, 2, [0xff, 0, 0]); + display.finishTile(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing BGRX blit images with true color via #blitImage', function () { + var data = []; + for (var i = 0; i < 16; i++) { + data[i * 4] = checked_data[i * 4 + 2]; + data[i * 4 + 1] = checked_data[i * 4 + 1]; + data[i * 4 + 2] = checked_data[i * 4]; + data[i * 4 + 3] = checked_data[i * 4 + 3]; + } + display.blitImage(0, 0, 4, 4, data, 0); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing RGB blit images with true color via #blitRgbImage', function () { + var data = []; + for (var i = 0; i < 16; i++) { + data[i * 3] = checked_data[i * 4]; + data[i * 3 + 1] = checked_data[i * 4 + 1]; + data[i * 3 + 2] = checked_data[i * 4 + 2]; + } + display.blitRgbImage(0, 0, 4, 4, data, 0); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing blit images from a data URL via #blitStringImage', function (done) { + var img_url = make_image_canvas(checked_data).toDataURL(); + display._drawCtx._act_drawImg = display._drawCtx.drawImage; + display._drawCtx.drawImage = function (img, x, y) { + this._act_drawImg(img, x, y); + expect(display).to.have.displayed(checked_data); + done(); + }; + display.blitStringImage(img_url, 0, 0); + }); + + it('should support drawing solid colors with color maps', function () { + display._true_color = false; + display.set_colourMap({ 0: [0xff, 0, 0], 1: [0, 0xff, 0] }); + display.fillRect(0, 0, 4, 4, 1); + display.fillRect(0, 0, 2, 2, 0); + display.fillRect(2, 2, 2, 2, 0); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing blit images with color maps', function () { + display._true_color = false; + display.set_colourMap({ 1: [0xff, 0, 0], 0: [0, 0xff, 0] }); + var data = [1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1].map(function (elem) { return [elem]; }); + display.blitImage(0, 0, 4, 4, data, 0); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing an image object via #drawImage', function () { + var img = make_image_canvas(checked_data); + display.drawImage(img, 0, 0); + expect(display).to.have.displayed(checked_data); + }); + } + + describe('(prefering native methods)', function () { drawing_tests.call(this, false); }); + describe('(prefering JavaScript)', function () { drawing_tests.call(this, true); }); + }); + + describe('the render queue processor', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false }); + display.resize(4, 4); + sinon.spy(display, '_scan_renderQ'); + this.old_requestAnimFrame = window.requestAnimFrame; + window.requestAnimFrame = function (cb) { + this.next_frame_cb = cb; + }.bind(this); + this.next_frame = function () { this.next_frame_cb(); }; + }); + + afterEach(function () { + window.requestAnimFrame = this.old_requestAnimFrame; + }); + + it('should try to process an item when it is pushed on, if nothing else is on the queue', function () { + display.renderQ_push({ type: 'noop' }); // does nothing + expect(display._scan_renderQ).to.have.been.calledOnce; + }); + + it('should not try to process an item when it is pushed on if we are waiting for other items', function () { + display._renderQ.length = 2; + display.renderQ_push({ type: 'noop' }); + expect(display._scan_renderQ).to.not.have.been.called; + }); + + it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () { + var img = { complete: false }; + display._renderQ = [{ type: 'img', x: 3, y: 4, img: img }, + { type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }]; + display.drawImage = sinon.spy(); + display.fillRect = sinon.spy(); + + display._scan_renderQ(); + expect(display.drawImage).to.not.have.been.called; + expect(display.fillRect).to.not.have.been.called; + + display._renderQ[0].img.complete = true; + this.next_frame(); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledOnce; + }); + + it('should draw a blit image on type "blit"', function () { + display.blitImage = sinon.spy(); + display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + expect(display.blitImage).to.have.been.calledOnce; + expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + }); + + it('should draw a blit RGB image on type "blitRgb"', function () { + display.blitRgbImage = sinon.spy(); + display.renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + expect(display.blitRgbImage).to.have.been.calledOnce; + expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + }); + + it('should copy a region on type "copy"', function () { + display.copyImage = sinon.spy(); + display.renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 }); + expect(display.copyImage).to.have.been.calledOnce; + expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6); + }); + + it('should fill a rect with a given color on type "fill"', function () { + display.fillRect = sinon.spy(); + display.renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]}); + expect(display.fillRect).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]); + }); + + it('should draw an image from an image object on type "img" (if complete)', function () { + display.drawImage = sinon.spy(); + display.renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } }); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4); + }); + }); +}); diff --git a/public/novnc/tests/test.helper.js b/public/novnc/tests/test.helper.js new file mode 100644 index 00000000..98009d2a --- /dev/null +++ b/public/novnc/tests/test.helper.js @@ -0,0 +1,262 @@ +// requires local modules: keysym, keysymdef, keyboard + +var assert = chai.assert; +var expect = chai.expect; + +describe('Helpers', function() { + "use strict"; + describe('keysymFromKeyCode', function() { + it('should map known keycodes to keysyms', function() { + expect(kbdUtil.keysymFromKeyCode(0x41, false), 'a').to.be.equal(0x61); + expect(kbdUtil.keysymFromKeyCode(0x41, true), 'A').to.be.equal(0x41); + expect(kbdUtil.keysymFromKeyCode(0xd, false), 'enter').to.be.equal(0xFF0D); + expect(kbdUtil.keysymFromKeyCode(0x11, false), 'ctrl').to.be.equal(0xFFE3); + expect(kbdUtil.keysymFromKeyCode(0x12, false), 'alt').to.be.equal(0xFFE9); + expect(kbdUtil.keysymFromKeyCode(0xe1, false), 'altgr').to.be.equal(0xFE03); + expect(kbdUtil.keysymFromKeyCode(0x1b, false), 'esc').to.be.equal(0xFF1B); + expect(kbdUtil.keysymFromKeyCode(0x26, false), 'up').to.be.equal(0xFF52); + }); + it('should return null for unknown keycodes', function() { + expect(kbdUtil.keysymFromKeyCode(0xc0, false), 'DK æ').to.be.null; + expect(kbdUtil.keysymFromKeyCode(0xde, false), 'DK ø').to.be.null; + }); + }); + + describe('keysyms.fromUnicode', function() { + it('should map ASCII characters to keysyms', function() { + expect(keysyms.fromUnicode('a'.charCodeAt())).to.have.property('keysym', 0x61); + expect(keysyms.fromUnicode('A'.charCodeAt())).to.have.property('keysym', 0x41); + }); + it('should map Latin-1 characters to keysyms', function() { + expect(keysyms.fromUnicode('ø'.charCodeAt())).to.have.property('keysym', 0xf8); + + expect(keysyms.fromUnicode('é'.charCodeAt())).to.have.property('keysym', 0xe9); + }); + it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function() { + expect(keysyms.fromUnicode('Š'.charCodeAt())).to.have.property('keysym', 0x01a9); + }); + it('should map characters which aren\'t in Latin1 *or* Windows-1252 to keysyms', function() { + expect(keysyms.fromUnicode('ŵ'.charCodeAt())).to.have.property('keysym', 0x1000175); + }); + it('should return undefined for unknown codepoints', function() { + expect(keysyms.fromUnicode('\n'.charCodeAt())).to.be.undefined; + expect(keysyms.fromUnicode('\u1F686'.charCodeAt())).to.be.undefined; + }); + }); + + describe('substituteCodepoint', function() { + it('should replace characters which don\'t have a keysym', function() { + expect(kbdUtil.substituteCodepoint('Ș'.charCodeAt())).to.equal('Ş'.charCodeAt()); + expect(kbdUtil.substituteCodepoint('ș'.charCodeAt())).to.equal('ş'.charCodeAt()); + expect(kbdUtil.substituteCodepoint('Ț'.charCodeAt())).to.equal('Ţ'.charCodeAt()); + expect(kbdUtil.substituteCodepoint('ț'.charCodeAt())).to.equal('ţ'.charCodeAt()); + }); + it('should pass other characters through unchanged', function() { + expect(kbdUtil.substituteCodepoint('T'.charCodeAt())).to.equal('T'.charCodeAt()); + }); + }); + + describe('nonCharacterKey', function() { + it('should recognize the right keys', function() { + expect(kbdUtil.nonCharacterKey({keyCode: 0xd}), 'enter').to.be.defined; + expect(kbdUtil.nonCharacterKey({keyCode: 0x08}), 'backspace').to.be.defined; + expect(kbdUtil.nonCharacterKey({keyCode: 0x09}), 'tab').to.be.defined; + expect(kbdUtil.nonCharacterKey({keyCode: 0x10}), 'shift').to.be.defined; + expect(kbdUtil.nonCharacterKey({keyCode: 0x11}), 'ctrl').to.be.defined; + expect(kbdUtil.nonCharacterKey({keyCode: 0x12}), 'alt').to.be.defined; + expect(kbdUtil.nonCharacterKey({keyCode: 0xe0}), 'meta').to.be.defined; + }); + it('should not recognize character keys', function() { + expect(kbdUtil.nonCharacterKey({keyCode: 'A'}), 'A').to.be.null; + expect(kbdUtil.nonCharacterKey({keyCode: '1'}), '1').to.be.null; + expect(kbdUtil.nonCharacterKey({keyCode: '.'}), '.').to.be.null; + expect(kbdUtil.nonCharacterKey({keyCode: ' '}), 'space').to.be.null; + }); + }); + + describe('getKeysym', function() { + it('should prefer char', function() { + expect(kbdUtil.getKeysym({char : 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x61); + }); + it('should use charCode if no char', function() { + expect(kbdUtil.getKeysym({char : '', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9); + expect(kbdUtil.getKeysym({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9); + expect(kbdUtil.getKeysym({char : 'hello', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9); + }); + it('should use keyCode if no charCode', function() { + expect(kbdUtil.getKeysym({keyCode: 0x42, which: 0x43, shiftKey: false})).to.have.property('keysym', 0x62); + expect(kbdUtil.getKeysym({keyCode: 0x42, which: 0x43, shiftKey: true})).to.have.property('keysym', 0x42); + }); + it('should use which if no keyCode', function() { + expect(kbdUtil.getKeysym({which: 0x43, shiftKey: false})).to.have.property('keysym', 0x63); + expect(kbdUtil.getKeysym({which: 0x43, shiftKey: true})).to.have.property('keysym', 0x43); + }); + it('should substitute where applicable', function() { + expect(kbdUtil.getKeysym({char : 'Ș'})).to.have.property('keysym', 0x1aa); + }); + }); + + describe('Modifier Sync', function() { // return a list of fake events necessary to fix modifier state + describe('Toggle all modifiers', function() { + var sync = kbdUtil.ModifierSync(); + it ('should do nothing if all modifiers are up as expected', function() { + expect(sync.keydown({ + keyCode: 0x41, + ctrlKey: false, + altKey: false, + altGraphKey: false, + shiftKey: false, + metaKey: false}) + ).to.have.lengthOf(0); + }); + it ('should synthesize events if all keys are unexpectedly down', function() { + var result = sync.keydown({ + keyCode: 0x41, + ctrlKey: true, + altKey: true, + altGraphKey: true, + shiftKey: true, + metaKey: true + }); + expect(result).to.have.lengthOf(5); + var keysyms = {}; + for (var i = 0; i < result.length; ++i) { + keysyms[result[i].keysym] = (result[i].type == 'keydown'); + } + expect(keysyms[0xffe3]); + expect(keysyms[0xffe9]); + expect(keysyms[0xfe03]); + expect(keysyms[0xffe1]); + expect(keysyms[0xffe7]); + }); + it ('should do nothing if all modifiers are down as expected', function() { + expect(sync.keydown({ + keyCode: 0x41, + ctrlKey: true, + altKey: true, + altGraphKey: true, + shiftKey: true, + metaKey: true + })).to.have.lengthOf(0); + }); + }); + describe('Toggle Ctrl', function() { + var sync = kbdUtil.ModifierSync(); + it('should sync if modifier is suddenly down', function() { + expect(sync.keydown({ + keyCode: 0x41, + ctrlKey: true, + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keydown'}]); + }); + it('should sync if modifier is suddenly up', function() { + expect(sync.keydown({ + keyCode: 0x41, + ctrlKey: false + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keyup'}]); + }); + }); + describe('Toggle Alt', function() { + var sync = kbdUtil.ModifierSync(); + it('should sync if modifier is suddenly down', function() { + expect(sync.keydown({ + keyCode: 0x41, + altKey: true, + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]); + }); + it('should sync if modifier is suddenly up', function() { + expect(sync.keydown({ + keyCode: 0x41, + altKey: false + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keyup'}]); + }); + }); + describe('Toggle AltGr', function() { + var sync = kbdUtil.ModifierSync(); + it('should sync if modifier is suddenly down', function() { + expect(sync.keydown({ + keyCode: 0x41, + altGraphKey: true, + })).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keydown'}]); + }); + it('should sync if modifier is suddenly up', function() { + expect(sync.keydown({ + keyCode: 0x41, + altGraphKey: false + })).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keyup'}]); + }); + }); + describe('Toggle Shift', function() { + var sync = kbdUtil.ModifierSync(); + it('should sync if modifier is suddenly down', function() { + expect(sync.keydown({ + keyCode: 0x41, + shiftKey: true, + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keydown'}]); + }); + it('should sync if modifier is suddenly up', function() { + expect(sync.keydown({ + keyCode: 0x41, + shiftKey: false + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keyup'}]); + }); + }); + describe('Toggle Meta', function() { + var sync = kbdUtil.ModifierSync(); + it('should sync if modifier is suddenly down', function() { + expect(sync.keydown({ + keyCode: 0x41, + metaKey: true, + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keydown'}]); + }); + it('should sync if modifier is suddenly up', function() { + expect(sync.keydown({ + keyCode: 0x41, + metaKey: false + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keyup'}]); + }); + }); + describe('Modifier keyevents', function() { + it('should not sync a modifier on its own events', function() { + expect(kbdUtil.ModifierSync().keydown({ + keyCode: 0x11, + ctrlKey: false + })).to.be.deep.equal([]); + expect(kbdUtil.ModifierSync().keydown({ + keyCode: 0x11, + ctrlKey: true + }), 'B').to.be.deep.equal([]); + }) + it('should update state on modifier keyevents', function() { + var sync = kbdUtil.ModifierSync(); + sync.keydown({ + keyCode: 0x11, + }); + expect(sync.keydown({ + keyCode: 0x41, + ctrlKey: true, + })).to.be.deep.equal([]); + }); + it('should sync other modifiers on ctrl events', function() { + expect(kbdUtil.ModifierSync().keydown({ + keyCode: 0x11, + altKey: true + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]); + }) + }); + describe('sync modifiers on non-key events', function() { + it('should generate sync events when receiving non-keyboard events', function() { + expect(kbdUtil.ModifierSync().syncAny({ + altKey: true + })).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]); + }); + }); + describe('do not treat shift as a modifier key', function() { + it('should not treat shift as a shortcut modifier', function() { + expect(kbdUtil.hasShortcutModifier([], {0xffe1 : true})).to.be.false; + }); + it('should not treat shift as a char modifier', function() { + expect(kbdUtil.hasCharModifier([], {0xffe1 : true})).to.be.false; + }); + }); + }); +}); diff --git a/public/novnc/tests/test.keyboard.js b/public/novnc/tests/test.keyboard.js new file mode 100644 index 00000000..2ac65af3 --- /dev/null +++ b/public/novnc/tests/test.keyboard.js @@ -0,0 +1,842 @@ +// requires local modules: input, keyboard, keysymdef +var assert = chai.assert; +var expect = chai.expect; + +/* jshint newcap: false, expr: true */ +describe('Key Event Pipeline Stages', function() { + "use strict"; + describe('Decode Keyboard Events', function() { + it('should pass events to the next stage', function(done) { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.be.an.object; + done(); + }).keydown({keyCode: 0x41}); + }); + it('should pass the right keysym through', function(done) { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt.keysym).to.be.deep.equal(keysyms.lookup(0x61)); + done(); + }).keypress({keyCode: 0x41}); + }); + it('should pass the right keyid through', function(done) { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.have.property('keyId', 0x41); + done(); + }).keydown({keyCode: 0x41}); + }); + it('should not sync modifiers on a keypress', function() { + // Firefox provides unreliable modifier state on keypress events + var count = 0; + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + ++count; + }).keypress({keyCode: 0x41, ctrlKey: true}); + expect(count).to.be.equal(1); + }); + it('should sync modifiers if necessary', function(done) { + var count = 0; + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + switch (count) { + case 0: // fake a ctrl keydown + expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xffe3), type: 'keydown'}); + ++count; + break; + case 1: + expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown', keysym: keysyms.lookup(0x61)}); + done(); + break; + } + }).keydown({keyCode: 0x41, ctrlKey: true}); + }); + it('should forward keydown events with the right type', function(done) { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'}); + done(); + }).keydown({keyCode: 0x41}); + }); + it('should forward keyup events with the right type', function(done) { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'}); + done(); + }).keyup({keyCode: 0x41}); + }); + it('should forward keypress events with the right type', function(done) { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'}); + done(); + }).keypress({keyCode: 0x41}); + }); + it('should generate stalls if a char modifier is down while a key is pressed', function(done) { + var count = 0; + KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) { + switch (count) { + case 0: // fake altgr + expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xfe03), type: 'keydown'}); + ++count; + break; + case 1: // stall before processing the 'a' keydown + expect(evt).to.be.deep.equal({type: 'stall'}); + ++count; + break; + case 2: // 'a' + expect(evt).to.be.deep.equal({ + type: 'keydown', + keyId: 0x41, + keysym: keysyms.lookup(0x61) + }); + + done(); + break; + } + }).keydown({keyCode: 0x41, altGraphKey: true}); + + }); + describe('suppress the right events at the right time', function() { + it('should suppress anything while a shortcut modifier is down', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {}); + + obj.keydown({keyCode: 0x11}); // press ctrl + expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.true; + expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.true; + expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.true; + expect(obj.keydown({keyCode: 0x3c})).to.be.true; // < key on DK Windows + expect(obj.keydown({keyCode: 0xde})).to.be.true; // Ø key on DK + }); + it('should suppress non-character keys', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {}); + + expect(obj.keydown({keyCode: 0x08}), 'a').to.be.true; + expect(obj.keydown({keyCode: 0x09}), 'b').to.be.true; + expect(obj.keydown({keyCode: 0x11}), 'd').to.be.true; + expect(obj.keydown({keyCode: 0x12}), 'e').to.be.true; + }); + it('should not suppress shift', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {}); + + expect(obj.keydown({keyCode: 0x10}), 'd').to.be.false; + }); + it('should generate event for shift keydown', function() { + var called = false; + var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.have.property('keysym'); + called = true; + }).keydown({keyCode: 0x10}); + expect(called).to.be.true; + }); + it('should not suppress character keys', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {}); + + expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false; + expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false; + expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false; + expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows + expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK + }); + it('should not suppress if a char modifier is down', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) {}); + + obj.keydown({keyCode: 0xe1}); // press altgr + expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false; + expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false; + expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false; + expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows + expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK + }); + }); + describe('Keypress and keyup events', function() { + it('should always suppress event propagation', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {}); + + expect(obj.keypress({keyCode: 'A'.charCodeAt()})).to.be.true; + expect(obj.keypress({keyCode: 0x3c})).to.be.true; // < key on DK Windows + expect(obj.keypress({keyCode: 0x11})).to.be.true; + + expect(obj.keyup({keyCode: 'A'.charCodeAt()})).to.be.true; + expect(obj.keyup({keyCode: 0x3c})).to.be.true; // < key on DK Windows + expect(obj.keyup({keyCode: 0x11})).to.be.true; + }); + it('should never generate stalls', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt.type).to.not.be.equal('stall'); + }); + + obj.keypress({keyCode: 'A'.charCodeAt()}); + obj.keypress({keyCode: 0x3c}); + obj.keypress({keyCode: 0x11}); + + obj.keyup({keyCode: 'A'.charCodeAt()}); + obj.keyup({keyCode: 0x3c}); + obj.keyup({keyCode: 0x11}); + }); + }); + describe('mark events if a char modifier is down', function() { + it('should not mark modifiers on a keydown event', function() { + var times_called = 0; + var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) { + switch (times_called++) { + case 0: //altgr + break; + case 1: // 'a' + expect(evt).to.not.have.property('escape'); + break; + } + }); + + obj.keydown({keyCode: 0xe1}); // press altgr + obj.keydown({keyCode: 'A'.charCodeAt()}); + }); + + it('should indicate on events if a single-key char modifier is down', function(done) { + var times_called = 0; + var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) { + switch (times_called++) { + case 0: //altgr + break; + case 1: // 'a' + expect(evt).to.be.deep.equal({ + type: 'keypress', + keyId: 'A'.charCodeAt(), + keysym: keysyms.lookup('a'.charCodeAt()), + escape: [0xfe03] + }); + done(); + return; + } + }); + + obj.keydown({keyCode: 0xe1}); // press altgr + obj.keypress({keyCode: 'A'.charCodeAt()}); + }); + it('should indicate on events if a multi-key char modifier is down', function(done) { + var times_called = 0; + var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xffe9, 0xffe3]), function(evt) { + switch (times_called++) { + case 0: //ctrl + break; + case 1: //alt + break; + case 2: // 'a' + expect(evt).to.be.deep.equal({ + type: 'keypress', + keyId: 'A'.charCodeAt(), + keysym: keysyms.lookup('a'.charCodeAt()), + escape: [0xffe9, 0xffe3] + }); + done(); + return; + } + }); + + obj.keydown({keyCode: 0x11}); // press ctrl + obj.keydown({keyCode: 0x12}); // press alt + obj.keypress({keyCode: 'A'.charCodeAt()}); + }); + it('should not consider a char modifier to be down on the modifier key itself', function() { + var obj = KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) { + expect(evt).to.not.have.property('escape'); + }); + + obj.keydown({keyCode: 0xe1}); // press altgr + + }); + }); + describe('add/remove keysym', function() { + it('should remove keysym from keydown if a char key and no modifier', function() { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'}); + }).keydown({keyCode: 0x41}); + }); + it('should not remove keysym from keydown if a shortcut modifier is down', function() { + var times_called = 0; + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + switch (times_called++) { + case 1: + expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'}); + break; + } + }).keydown({keyCode: 0x41, ctrlKey: true}); + expect(times_called).to.be.equal(2); + }); + it('should not remove keysym from keydown if a char modifier is down', function() { + var times_called = 0; + KeyEventDecoder(kbdUtil.ModifierSync([0xfe03]), function(evt) { + switch (times_called++) { + case 2: + expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'}); + break; + } + }).keydown({keyCode: 0x41, altGraphKey: true}); + expect(times_called).to.be.equal(3); + }); + it('should not remove keysym from keydown if key is noncharacter', function() { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt, 'bacobjpace').to.be.deep.equal({keyId: 0x09, keysym: keysyms.lookup(0xff09), type: 'keydown'}); + }).keydown({keyCode: 0x09}); + + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt, 'ctrl').to.be.deep.equal({keyId: 0x11, keysym: keysyms.lookup(0xffe3), type: 'keydown'}); + }).keydown({keyCode: 0x11}); + }); + it('should never remove keysym from keypress', function() { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'}); + }).keypress({keyCode: 0x41}); + }); + it('should never remove keysym from keyup', function() { + KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { + expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'}); + }).keyup({keyCode: 0x41}); + }); + }); + // on keypress, keyup(?), always set keysym + // on keydown, only do it if we don't expect a keypress: if noncharacter OR modifier is down + }); + + describe('Verify that char modifiers are active', function() { + it('should pass keydown events through if there is no stall', function(done) { + var obj = VerifyCharModifier(function(evt){ + expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + done(); + })({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + }); + it('should pass keyup events through if there is no stall', function(done) { + var obj = VerifyCharModifier(function(evt){ + expect(evt).to.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + done(); + })({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + }); + it('should pass keypress events through if there is no stall', function(done) { + var obj = VerifyCharModifier(function(evt){ + expect(evt).to.deep.equal({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + done(); + })({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + }); + it('should not pass stall events through', function(done){ + var obj = VerifyCharModifier(function(evt){ + // should only be called once, for the keydown + expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + done(); + }); + + obj({type: 'stall'}); + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + }); + it('should merge keydown and keypress events if they come after a stall', function(done) { + var next_called = false; + var obj = VerifyCharModifier(function(evt){ + // should only be called once, for the keydown + expect(next_called).to.be.false; + next_called = true; + expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44)}); + done(); + }); + + obj({type: 'stall'}); + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)}); + expect(next_called).to.be.false; + }); + it('should preserve modifier attribute when merging if keysyms differ', function(done) { + var next_called = false; + var obj = VerifyCharModifier(function(evt){ + // should only be called once, for the keydown + expect(next_called).to.be.false; + next_called = true; + expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44), escape: [0xffe3]}); + done(); + }); + + obj({type: 'stall'}); + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44), escape: [0xffe3]}); + expect(next_called).to.be.false; + }); + it('should not preserve modifier attribute when merging if keysyms are the same', function() { + var obj = VerifyCharModifier(function(evt){ + expect(evt).to.not.have.property('escape'); + }); + + obj({type: 'stall'}); + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x42), escape: [0xffe3]}); + }); + it('should not merge keydown and keypress events if there is no stall', function(done) { + var times_called = 0; + var obj = VerifyCharModifier(function(evt){ + switch(times_called) { + case 0: + expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + break; + case 1: + expect(evt).to.deep.equal({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)}); + done(); + break; + } + + ++times_called; + }); + + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)}); + }); + it('should not merge keydown and keypress events if separated by another event', function(done) { + var times_called = 0; + var obj = VerifyCharModifier(function(evt){ + switch(times_called) { + case 0: + expect(evt,1).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + break; + case 1: + expect(evt,2).to.deep.equal({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)}); + break; + case 2: + expect(evt,3).to.deep.equal({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)}); + done(); + break; + } + + ++times_called; + }); + + obj({type: 'stall'}); + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + obj({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)}); + obj({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)}); + }); + }); + + describe('Track Key State', function() { + it('should do nothing on keyup events if no keys are down', function() { + var obj = TrackKeyState(function(evt) { + expect(true).to.be.false; + }); + obj({type: 'keyup', keyId: 0x41}); + }); + it('should insert into the queue on keydown if no keys are down', function() { + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + } + elem = null; + }); + + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keyup', keyId: 0x41}; + obj(elem); + expect(elem).to.be.null; + expect(times_called).to.be.equal(2); + }); + it('should insert into the queue on keypress if no keys are down', function() { + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + } + elem = null; + }); + + expect(elem).to.be.null; + elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keyup', keyId: 0x41}; + obj(elem); + expect(elem).to.be.null; + expect(times_called).to.be.equal(2); + }); + it('should add keysym to last key entry if keyId matches', function() { + // this implies that a single keyup will release both keysyms + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + elem = null; + } + }); + + expect(elem).to.be.null; + elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)}; + keysymsdown[keysyms.lookup(0x43).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keyup', keyId: 0x41}; + obj(elem); + expect(times_called).to.be.equal(4); + }); + it('should create new key entry if keyId matches and keysym does not', function() { + // this implies that a single keyup will release both keysyms + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + elem = null; + } + }); + + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)}; + keysymsdown[keysyms.lookup(0x43).keysym] = true; + obj(elem); + expect(times_called).to.be.equal(2); + expect(elem).to.be.null; + elem = {type: 'keyup', keyId: 0}; + obj(elem); + expect(times_called).to.be.equal(3); + elem = {type: 'keyup', keyId: 0}; + obj(elem); + expect(times_called).to.be.equal(4); + }); + it('should merge key entry if keyIds are zero and keysyms match', function() { + // this implies that a single keyup will release both keysyms + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + elem = null; + } + }); + + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(times_called).to.be.equal(2); + expect(elem).to.be.null; + elem = {type: 'keyup', keyId: 0}; + obj(elem); + expect(times_called).to.be.equal(3); + }); + it('should add keysym as separate entry if keyId does not match last event', function() { + // this implies that separate keyups are required + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + elem = null; + } + }); + + expect(elem).to.be.null; + elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keypress', keyId: 0x42, keysym: keysyms.lookup(0x43)}; + keysymsdown[keysyms.lookup(0x43).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keyup', keyId: 0x41}; + obj(elem); + expect(times_called).to.be.equal(4); + elem = {type: 'keyup', keyId: 0x42}; + obj(elem); + expect(times_called).to.be.equal(4); + }); + it('should add keysym as separate entry if keyId does not match last event and first is zero', function() { + // this implies that separate keyups are required + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + elem = null; + } + }); + + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x43)}; + keysymsdown[keysyms.lookup(0x43).keysym] = true; + obj(elem); + expect(elem).to.be.null; + expect(times_called).to.be.equal(2); + elem = {type: 'keyup', keyId: 0}; + obj(elem); + expect(times_called).to.be.equal(3); + elem = {type: 'keyup', keyId: 0x42}; + obj(elem); + expect(times_called).to.be.equal(4); + }); + it('should add keysym as separate entry if keyId does not match last event and second is zero', function() { + // this implies that a separate keyups are required + var times_called = 0; + var elem = null; + var keysymsdown = {}; + var obj = TrackKeyState(function(evt) { + ++times_called; + if (elem.type == 'keyup') { + expect(evt).to.have.property('keysym'); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + delete keysymsdown[evt.keysym.keysym]; + } + else { + expect(evt).to.be.deep.equal(elem); + expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined; + elem = null; + } + }); + + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}; + keysymsdown[keysyms.lookup(0x42).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)}; + keysymsdown[keysyms.lookup(0x43).keysym] = true; + obj(elem); + expect(elem).to.be.null; + elem = {type: 'keyup', keyId: 0x41}; + obj(elem); + expect(times_called).to.be.equal(3); + elem = {type: 'keyup', keyId: 0}; + obj(elem); + expect(times_called).to.be.equal(4); + }); + it('should pop matching key event on keyup', function() { + var times_called = 0; + var obj = TrackKeyState(function(evt) { + switch (times_called++) { + case 0: + case 1: + case 2: + expect(evt.type).to.be.equal('keydown'); + break; + case 3: + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x42, keysym: keysyms.lookup(0x62)}); + break; + } + }); + + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)}); + obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)}); + obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)}); + obj({type: 'keyup', keyId: 0x42}); + expect(times_called).to.equal(4); + }); + it('should pop the first zero keyevent on keyup with zero keyId', function() { + var times_called = 0; + var obj = TrackKeyState(function(evt) { + switch (times_called++) { + case 0: + case 1: + case 2: + expect(evt.type).to.be.equal('keydown'); + break; + case 3: + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x61)}); + break; + } + }); + + obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x61)}); + obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x62)}); + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x63)}); + obj({type: 'keyup', keyId: 0x0}); + expect(times_called).to.equal(4); + }); + it('should pop the last keyevents keysym if no match is found for keyId', function() { + var times_called = 0; + var obj = TrackKeyState(function(evt) { + switch (times_called++) { + case 0: + case 1: + case 2: + expect(evt.type).to.be.equal('keydown'); + break; + case 3: + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x44, keysym: keysyms.lookup(0x63)}); + break; + } + }); + + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)}); + obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)}); + obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)}); + obj({type: 'keyup', keyId: 0x44}); + expect(times_called).to.equal(4); + }); + describe('Firefox sends keypress even when keydown is suppressed', function() { + it('should discard the keypress', function() { + var times_called = 0; + var obj = TrackKeyState(function(evt) { + expect(times_called).to.be.equal(0); + ++times_called; + }); + + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + expect(times_called).to.be.equal(1); + obj({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)}); + }); + }); + describe('releaseAll', function() { + it('should do nothing if no keys have been pressed', function() { + var times_called = 0; + var obj = TrackKeyState(function(evt) { + ++times_called; + }); + obj({type: 'releaseall'}); + expect(times_called).to.be.equal(0); + }); + it('should release the keys that have been pressed', function() { + var times_called = 0; + var obj = TrackKeyState(function(evt) { + switch (times_called++) { + case 2: + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x41)}); + break; + case 3: + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x42)}); + break; + } + }); + obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)}); + obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x42)}); + expect(times_called).to.be.equal(2); + obj({type: 'releaseall'}); + expect(times_called).to.be.equal(4); + obj({type: 'releaseall'}); + expect(times_called).to.be.equal(4); + }); + }); + + }); + + describe('Escape Modifiers', function() { + describe('Keydown', function() { + it('should pass through when a char modifier is not down', function() { + var times_called = 0; + EscapeModifiers(function(evt) { + expect(times_called).to.be.equal(0); + ++times_called; + expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + })({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + expect(times_called).to.be.equal(1); + }); + it('should generate fake undo/redo events when a char modifier is down', function() { + var times_called = 0; + EscapeModifiers(function(evt) { + switch(times_called++) { + case 0: + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe9)}); + break; + case 1: + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe3)}); + break; + case 2: + expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xffe9, 0xffe3]}); + break; + case 3: + expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe9)}); + break; + case 4: + expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe3)}); + break; + } + })({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xffe9, 0xffe3]}); + expect(times_called).to.be.equal(5); + }); + }); + describe('Keyup', function() { + it('should pass through when a char modifier is down', function() { + var times_called = 0; + EscapeModifiers(function(evt) { + expect(times_called).to.be.equal(0); + ++times_called; + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xfe03]}); + })({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xfe03]}); + expect(times_called).to.be.equal(1); + }); + it('should pass through when a char modifier is not down', function() { + var times_called = 0; + EscapeModifiers(function(evt) { + expect(times_called).to.be.equal(0); + ++times_called; + expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + })({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)}); + expect(times_called).to.be.equal(1); + }); + }); + }); +}); diff --git a/public/novnc/tests/test.rfb.js b/public/novnc/tests/test.rfb.js new file mode 100644 index 00000000..a0f2fa70 --- /dev/null +++ b/public/novnc/tests/test.rfb.js @@ -0,0 +1,1929 @@ +// requires local modules: util, websock, rfb, keyboard, keysym, keysymdef, input, inflator, des, display +// requires test modules: fake.websocket, assertions +/* jshint expr: true */ +var assert = chai.assert; +var expect = chai.expect; + +function make_rfb (extra_opts) { + if (!extra_opts) { + extra_opts = {}; + } + + extra_opts.target = extra_opts.target || document.createElement('canvas'); + return new RFB(extra_opts); +} + +describe('Remote Frame Buffer Protocol Client', function() { + "use strict"; + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + before(function () { + this.clock = sinon.useFakeTimers(); + // Use a single set of buffers instead of reallocating to + // speed up tests + var sock = new Websock(); + var _sQ = new Uint8Array(sock._sQbufferSize); + var rQ = new Uint8Array(sock._rQbufferSize); + + Websock.prototype._old_allocate_buffers = Websock.prototype._allocate_buffers; + Websock.prototype._allocate_buffers = function () { + this._sQ = _sQ; + this._rQ = rQ; + }; + + }); + + after(function () { + Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers; + this.clock.restore(); + }); + + describe('Public API Basic Behavior', function () { + var client; + beforeEach(function () { + client = make_rfb(); + }); + + describe('#connect', function () { + beforeEach(function () { client._updateState = sinon.spy(); }); + + it('should set the current state to "connect"', function () { + client.connect('host', 8675); + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('connect'); + }); + + it('should fail if we are missing a host', function () { + sinon.spy(client, '_fail'); + client.connect(undefined, 8675); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should fail if we are missing a port', function () { + sinon.spy(client, '_fail'); + client.connect('abc'); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should not update the state if we are missing a host or port', function () { + sinon.spy(client, '_fail'); + client.connect('abc'); + expect(client._fail).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('failed'); + }); + }); + + describe('#disconnect', function () { + beforeEach(function () { client._updateState = sinon.spy(); }); + + it('should set the current state to "disconnect"', function () { + client.disconnect(); + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('disconnect'); + }); + + it('should unregister error event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('error'); + }); + + it('should unregister message event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('message'); + }); + + it('should unregister open event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('open'); + }); + }); + + describe('#sendPassword', function () { + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should set the state to "Authentication"', function () { + client._rfb_state = "blah"; + client.sendPassword('pass'); + expect(client._rfb_state).to.equal('Authentication'); + }); + + it('should call init_msg "soon"', function () { + client._init_msg = sinon.spy(); + client.sendPassword('pass'); + this.clock.tick(5); + expect(client._init_msg).to.have.been.calledOnce; + }); + }); + + describe('#sendCtrlAlDel', function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'flush'); + client._rfb_state = "normal"; + client._view_only = false; + }); + + it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { + var expected = {_sQ: new Uint8Array(48), _sQlen: 0}; + RFB.messages.keyEvent(expected, 0xFFE3, 1); + RFB.messages.keyEvent(expected, 0xFFE9, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 0); + RFB.messages.keyEvent(expected, 0xFFE9, 0); + RFB.messages.keyEvent(expected, 0xFFE3, 0); + + client.sendCtrlAltDel(); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send the keys if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.sendCtrlAltDel(); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send the keys if we are set as view_only', function () { + client._view_only = true; + client.sendCtrlAltDel(); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe('#sendKey', function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'flush'); + client._rfb_state = "normal"; + client._view_only = false; + }); + + it('should send a single key with the given code and state (down = true)', function () { + var expected = {_sQ: new Uint8Array(8), _sQlen: 0}; + RFB.messages.keyEvent(expected, 123, 1); + client.sendKey(123, true); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should send both a down and up event if the state is not specified', function () { + var expected = {_sQ: new Uint8Array(16), _sQlen: 0}; + RFB.messages.keyEvent(expected, 123, 1); + RFB.messages.keyEvent(expected, 123, 0); + client.sendKey(123); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send the key if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.sendKey(123); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send the key if we are set as view_only', function () { + client._view_only = true; + client.sendKey(123); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe('#clipboardPasteFrom', function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'flush'); + client._rfb_state = "normal"; + client._view_only = false; + }); + + it('should send the given text in a paste event', function () { + var expected = {_sQ: new Uint8Array(11), _sQlen: 0}; + RFB.messages.clientCutText(expected, 'abc'); + client.clipboardPasteFrom('abc'); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send the text if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.clipboardPasteFrom('abc'); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe("#requestDesktopSize", function () { + beforeEach(function() { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'flush'); + client._rfb_state = "normal"; + client._view_only = false; + client._supportsSetDesktopSize = true; + }); + + it('should send the request with the given width and height', function () { + var expected = [251]; + expected.push8(0); // padding + expected.push16(1); // width + expected.push16(2); // height + expected.push8(1); // number-of-screens + expected.push8(0); // padding before screen array + expected.push32(0); // id + expected.push16(0); // x-position + expected.push16(0); // y-position + expected.push16(1); // width + expected.push16(2); // height + expected.push32(0); // flags + + client.requestDesktopSize(1, 2); + expect(client._sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should not send the request if the client has not recieved a ExtendedDesktopSize rectangle', function () { + client._supportsSetDesktopSize = false; + client.requestDesktopSize(1,2); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send the request if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.requestDesktopSize(1,2); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe("XVP operations", function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'flush'); + client._rfb_state = "normal"; + client._view_only = false; + client._rfb_xvp_ver = 1; + }); + + it('should send the shutdown signal on #xvpShutdown', function () { + client.xvpShutdown(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x02])); + }); + + it('should send the reboot signal on #xvpReboot', function () { + client.xvpReboot(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x03])); + }); + + it('should send the reset signal on #xvpReset', function () { + client.xvpReset(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x04])); + }); + + it('should support sending arbitrary XVP operations via #xvpOp', function () { + client.xvpOp(1, 7); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x07])); + }); + + it('should not send XVP operations with higher versions than we support', function () { + expect(client.xvpOp(2, 7)).to.be.false; + expect(client._sock.flush).to.not.have.been.called; + }); + }); + }); + + describe('Misc Internals', function () { + describe('#_updateState', function () { + var client; + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + client = make_rfb(); + }); + + afterEach(function () { + this.clock.restore(); + }); + + it('should clear the disconnect timer if the state is not disconnect', function () { + var spy = sinon.spy(); + client._disconnTimer = setTimeout(spy, 50); + client._updateState('normal'); + this.clock.tick(51); + expect(spy).to.not.have.been.called; + expect(client._disconnTimer).to.be.null; + }); + }); + }); + + describe('Page States', function () { + describe('loaded', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should close any open WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('loaded'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + describe('disconnected', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should close any open WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('disconnected'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + describe('connect', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should reset the variable states', function () { + sinon.spy(client, '_init_vars'); + client._updateState('connect'); + expect(client._init_vars).to.have.been.calledOnce; + }); + + it('should actually connect to the websocket', function () { + sinon.spy(client._sock, 'open'); + client._updateState('connect'); + expect(client._sock.open).to.have.been.calledOnce; + }); + + it('should use wss:// to connect if encryption is enabled', function () { + sinon.spy(client._sock, 'open'); + client.set_encrypt(true); + client._updateState('connect'); + expect(client._sock.open.args[0][0]).to.contain('wss://'); + }); + + it('should use ws:// to connect if encryption is not enabled', function () { + sinon.spy(client._sock, 'open'); + client.set_encrypt(true); + client._updateState('connect'); + expect(client._sock.open.args[0][0]).to.contain('wss://'); + }); + + it('should use a uri with the host, port, and path specified to connect', function () { + sinon.spy(client._sock, 'open'); + client.set_encrypt(false); + client._rfb_host = 'HOST'; + client._rfb_port = 8675; + client._rfb_path = 'PATH'; + client._updateState('connect'); + expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); + }); + + it('should attempt to close the websocket before we open an new one', function () { + sinon.spy(client._sock, 'close'); + client._updateState('connect'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + describe('disconnect', function () { + var client; + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + client = make_rfb(); + client.connect('host', 8675); + }); + + afterEach(function () { + this.clock.restore(); + }); + + it('should fail if we do not call Websock.onclose within the disconnection timeout', function () { + client._sock._websocket.close = function () {}; // explicitly don't call onclose + client._updateState('disconnect'); + this.clock.tick(client.get_disconnectTimeout() * 1000); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { + client._updateState('disconnect'); + this.clock.tick(client.get_disconnectTimeout() * 500); + client._sock._websocket.close(); + this.clock.tick(client.get_disconnectTimeout() * 500 + 1); + expect(client._rfb_state).to.equal('disconnected'); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('disconnect'); + expect(client._sock.close).to.have.been.calledTwice; // once on loaded, once on disconnect + }); + }); + + describe('failed', function () { + var client; + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + client = make_rfb(); + client.connect('host', 8675); + }); + + afterEach(function () { + this.clock.restore(); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('failed'); + expect(client._sock.close).to.have.been.called; + }); + + it('should transition to disconnected but stay in failed state', function () { + client.set_onUpdateState(sinon.spy()); + client._updateState('failed'); + this.clock.tick(50); + expect(client._rfb_state).to.equal('failed'); + + var onUpdateState = client.get_onUpdateState(); + expect(onUpdateState).to.have.been.called; + // it should be specifically the last call + expect(onUpdateState.args[onUpdateState.args.length - 1][1]).to.equal('disconnected'); + expect(onUpdateState.args[onUpdateState.args.length - 1][2]).to.equal('failed'); + }); + + }); + + describe('fatal', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should close any open WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('fatal'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + // NB(directxman12): Normal does *nothing* in updateState + }); + + describe('Protocol Initialization States', function () { + describe('ProtocolVersion', function () { + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + this.clock.restore(); + }); + + function send_ver (ver, client) { + var arr = new Uint8Array(12); + for (var i = 0; i < ver.length; i++) { + arr[i+4] = ver.charCodeAt(i); + } + arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' '; + arr[11] = '\n'; + client._sock._websocket._receive_data(arr); + } + + describe('version parsing', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + }); + + it('should interpret version 000.000 as a repeater', function () { + client._repeaterID = '\x01\x02\x03\x04\x05'; + send_ver('000.000', client); + expect(client._rfb_version).to.equal(0); + + var sent_data = client._sock._websocket._get_sent_data(); + expect(new Uint8Array(sent_data.buffer, 0, 5)).to.array.equal(new Uint8Array([1, 2, 3, 4, 5])); + }); + + it('should interpret version 003.003 as version 3.3', function () { + send_ver('003.003', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.006 as version 3.3', function () { + send_ver('003.006', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.889 as version 3.3', function () { + send_ver('003.889', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.007 as version 3.7', function () { + send_ver('003.007', client); + expect(client._rfb_version).to.equal(3.7); + }); + + it('should interpret version 003.008 as version 3.8', function () { + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.000 as version 3.8', function () { + send_ver('004.000', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.001 as version 3.8', function () { + send_ver('004.001', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should fail on an invalid version', function () { + send_ver('002.000', client); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + var client; + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + }); + + it('should handle two step repeater negotiation', function () { + client._repeaterID = '\x01\x02\x03\x04\x05'; + + send_ver('000.000', client); + expect(client._rfb_version).to.equal(0); + var sent_data = client._sock._websocket._get_sent_data(); + expect(new Uint8Array(sent_data.buffer, 0, 5)).to.array.equal(new Uint8Array([1, 2, 3, 4, 5])); + expect(sent_data).to.have.length(250); + + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should initialize the flush interval', function () { + client._sock.flush = sinon.spy(); + send_ver('003.008', client); + this.clock.tick(100); + expect(client._sock.flush).to.have.been.calledThrice; + }); + + it('should send back the interpreted version', function () { + send_ver('004.000', client); + + var expected_str = 'RFB 003.008\n'; + var expected = []; + for (var i = 0; i < expected_str.length; i++) { + expected[i] = expected_str.charCodeAt(i); + } + + expect(client._sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should transition to the Security state on successful negotiation', function () { + send_ver('003.008', client); + expect(client._rfb_state).to.equal('Security'); + }); + }); + + describe('Security', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + }); + + it('should simply receive the auth scheme when for versions < 3.7', function () { + client._rfb_version = 3.6; + var auth_scheme_raw = [1, 2, 3, 4]; + var auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) + + (auth_scheme_raw[2] << 8) + auth_scheme_raw[3]; + client._sock._websocket._receive_data(auth_scheme_raw); + expect(client._rfb_auth_scheme).to.equal(auth_scheme); + }); + + it('should choose for the most prefered scheme possible for versions >= 3.7', function () { + client._rfb_version = 3.7; + var auth_schemes = [2, 1, 2]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_auth_scheme).to.equal(2); + expect(client._sock).to.have.sent(new Uint8Array([2])); + }); + + it('should fail if there are no supported schemes for versions >= 3.7', function () { + client._rfb_version = 3.7; + var auth_schemes = [1, 32]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () { + client._rfb_version = 3.7; + var failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(failure_data); + + expect(client._fail).to.have.been.calledTwice; + expect(client._fail).to.have.been.calledWith('Security failure: whoops'); + }); + + it('should transition to the Authentication state and continue on successful negotiation', function () { + client._rfb_version = 3.7; + var auth_schemes = [1, 1]; + client._negotiate_authentication = sinon.spy(); + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_state).to.equal('Authentication'); + expect(client._negotiate_authentication).to.have.been.calledOnce; + }); + }); + + describe('Authentication', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + }); + + function send_security(type, cl) { + cl._sock._websocket._receive_data(new Uint8Array([1, type])); + } + + it('should fail on auth scheme 0 (pre 3.7) with the given message', function () { + client._rfb_version = 3.6; + var err_msg = "Whoopsies"; + var data = [0, 0, 0, 0]; + var err_len = err_msg.length; + data.push32(err_len); + for (var i = 0; i < err_len; i++) { + data.push(err_msg.charCodeAt(i)); + } + + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(new Uint8Array(data)); + expect(client._rfb_state).to.equal('failed'); + expect(client._fail).to.have.been.calledWith('Auth failure: Whoopsies'); + }); + + it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () { + client._rfb_version = 3.8; + send_security(1, client); + expect(client._rfb_state).to.equal('SecurityResult'); + }); + + it('should transition straight to ClientInitialisation on "no auth" for versions < 3.8', function () { + client._rfb_version = 3.7; + sinon.spy(client, '_updateState'); + send_security(1, client); + expect(client._updateState).to.have.been.calledWith('ClientInitialisation'); + expect(client._rfb_state).to.equal('ServerInitialisation'); + }); + + it('should fail on an unknown auth scheme', function () { + client._rfb_version = 3.8; + send_security(57, client); + expect(client._rfb_state).to.equal('failed'); + }); + + describe('VNC Authentication (type 2) Handler', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + client._rfb_version = 3.8; + }); + + it('should transition to the "password" state if missing a password', function () { + send_security(2, client); + expect(client._rfb_state).to.equal('password'); + }); + + it('should encrypt the password with DES and then send it back', function () { + client._rfb_password = 'passwd'; + send_security(2, client); + client._sock._websocket._get_sent_data(); // skip the choice of auth reply + + var challenge = []; + for (var i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); + + var des_pass = RFB.genDES('passwd', challenge); + expect(client._sock).to.have.sent(new Uint8Array(des_pass)); + }); + + it('should transition to SecurityResult immediately after sending the password', function () { + client._rfb_password = 'passwd'; + send_security(2, client); + + var challenge = []; + for (var i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); + + expect(client._rfb_state).to.equal('SecurityResult'); + }); + }); + + describe('XVP Authentication (type 22) Handler', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + client._rfb_version = 3.8; + }); + + it('should fall through to standard VNC authentication upon completion', function () { + client.set_xvp_password_sep('#'); + client._rfb_password = 'user#target#password'; + client._negotiate_std_vnc_auth = sinon.spy(); + send_security(22, client); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + }); + + it('should transition to the "password" state if the passwords is missing', function() { + send_security(22, client); + expect(client._rfb_state).to.equal('password'); + }); + + it('should transition to the "password" state if the passwords is improperly formatted', function() { + client._rfb_password = 'user@target'; + send_security(22, client); + expect(client._rfb_state).to.equal('password'); + }); + + it('should split the password, send the first two parts, and pass on the last part', function () { + client.set_xvp_password_sep('#'); + client._rfb_password = 'user#target#password'; + client._negotiate_std_vnc_auth = sinon.spy(); + + send_security(22, client); + + expect(client._rfb_password).to.equal('password'); + + var expected = [22, 4, 6]; // auth selection, len user, len target + for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } + + expect(client._sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('TightVNC Authentication (type 16) Handler', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + client._rfb_version = 3.8; + send_security(16, client); + client._sock._websocket._get_sent_data(); // skip the security reply + }); + + function send_num_str_pairs(pairs, client) { + var pairs_len = pairs.length; + var data = []; + data.push32(pairs_len); + + for (var i = 0; i < pairs_len; i++) { + data.push32(pairs[i][0]); + var j; + for (j = 0; j < 4; j++) { + data.push(pairs[i][1].charCodeAt(j)); + } + for (j = 0; j < 8; j++) { + data.push(pairs[i][2].charCodeAt(j)); + } + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + } + + it('should skip tunnel negotiation if no tunnels are requested', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.true; + }); + + it('should fail if no supported tunnels are listed', function () { + send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should choose the notunnel tunnel type', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); + }); + + it('should continue to sub-auth negotiation after tunnel negotiation', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client); + client._sock._websocket._get_sent_data(); // skip the tunnel choice here + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_state).to.equal('SecurityResult'); + }); + + /*it('should attempt to use VNC auth over no auth when possible', function () { + client._rfb_tightvnc = true; + client._negotiate_std_vnc_auth = sinon.spy(); + send_num_str_pairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client); + expect(client._sock).to.have.sent([0, 0, 0, 1]); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + expect(client._rfb_auth_scheme).to.equal(2); + });*/ // while this would make sense, the original code doesn't actually do this + + it('should accept the "no auth" auth type and transition to SecurityResult', function () { + client._rfb_tightvnc = true; + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_state).to.equal('SecurityResult'); + }); + + it('should accept VNC authentication and transition to that', function () { + client._rfb_tightvnc = true; + client._negotiate_std_vnc_auth = sinon.spy(); + send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2])); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + expect(client._rfb_auth_scheme).to.equal(2); + }); + + it('should fail if there are no supported auth types', function () { + client._rfb_tightvnc = true; + send_num_str_pairs([[23, 'stdv', 'badval__']], client); + expect(client._rfb_state).to.equal('failed'); + }); + }); + }); + + describe('SecurityResult', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'SecurityResult'; + }); + + it('should fall through to ClientInitialisation on a response code of 0', function () { + client._updateState = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('ClientInitialisation'); + }); + + it('should fail on an error code of 1 with the given message for versions >= 3.8', function () { + client._rfb_version = 3.8; + sinon.spy(client, '_fail'); + var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(client._rfb_state).to.equal('failed'); + expect(client._fail).to.have.been.calledWith('whoops'); + }); + + it('should fail on an error code of 1 with a standard message for version < 3.8', function () { + client._rfb_version = 3.7; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + describe('ClientInitialisation', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'SecurityResult'; + }); + + it('should transition to the ServerInitialisation state', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_state).to.equal('ServerInitialisation'); + }); + + it('should send 1 if we are in shared mode', function () { + client.set_shared(true); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent(new Uint8Array([1])); + }); + + it('should send 0 if we are not in shared mode', function () { + client.set_shared(false); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent(new Uint8Array([0])); + }); + }); + + describe('ServerInitialisation', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'ServerInitialisation'; + }); + + function send_server_init(opts, client) { + var full_opts = { width: 10, height: 12, bpp: 24, depth: 24, big_endian: 0, + true_color: 1, red_max: 255, green_max: 255, blue_max: 255, + red_shift: 16, green_shift: 8, blue_shift: 0, name: 'a name' }; + for (var opt in opts) { + full_opts[opt] = opts[opt]; + } + var data = []; + + data.push16(full_opts.width); + data.push16(full_opts.height); + + data.push(full_opts.bpp); + data.push(full_opts.depth); + data.push(full_opts.big_endian); + data.push(full_opts.true_color); + + data.push16(full_opts.red_max); + data.push16(full_opts.green_max); + data.push16(full_opts.blue_max); + data.push8(full_opts.red_shift); + data.push8(full_opts.green_shift); + data.push8(full_opts.blue_shift); + + // padding + data.push8(0); + data.push8(0); + data.push8(0); + + client._sock._websocket._receive_data(new Uint8Array(data)); + + var name_data = []; + name_data.push32(full_opts.name.length); + for (var i = 0; i < full_opts.name.length; i++) { + name_data.push(full_opts.name.charCodeAt(i)); + } + client._sock._websocket._receive_data(new Uint8Array(name_data)); + } + + it('should set the framebuffer width and height', function () { + send_server_init({ width: 32, height: 84 }, client); + expect(client._fb_width).to.equal(32); + expect(client._fb_height).to.equal(84); + }); + + // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them + + it('should set the framebuffer name and call the callback', function () { + client.set_onDesktopName(sinon.spy()); + send_server_init({ name: 'some name' }, client); + + var spy = client.get_onDesktopName(); + expect(client._fb_name).to.equal('some name'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal('some name'); + }); + + it('should handle the extended init message of the tight encoding', function () { + // NB(sross): we don't actually do anything with it, so just test that we can + // read it w/o throwing an error + client._rfb_tightvnc = true; + send_server_init({}, client); + + var tight_data = []; + tight_data.push16(1); + tight_data.push16(2); + tight_data.push16(3); + tight_data.push16(0); + for (var i = 0; i < 16 + 32 + 48; i++) { + tight_data.push(i); + } + client._sock._websocket._receive_data(tight_data); + + expect(client._rfb_state).to.equal('normal'); + }); + + it('should set the true color mode on the display to the configuration variable', function () { + client.set_true_color(false); + sinon.spy(client._display, 'set_true_color'); + send_server_init({ true_color: 1 }, client); + expect(client._display.set_true_color).to.have.been.calledOnce; + expect(client._display.set_true_color).to.have.been.calledWith(false); + }); + + it('should call the resize callback and resize the display', function () { + client.set_onFBResize(sinon.spy()); + sinon.spy(client._display, 'resize'); + send_server_init({ width: 27, height: 32 }, client); + + var spy = client.get_onFBResize(); + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(27, 32); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal(27); + expect(spy.args[0][2]).to.equal(32); + }); + + it('should grab the mouse and keyboard', function () { + sinon.spy(client._keyboard, 'grab'); + sinon.spy(client._mouse, 'grab'); + send_server_init({}, client); + expect(client._keyboard.grab).to.have.been.calledOnce; + expect(client._mouse.grab).to.have.been.calledOnce; + }); + + it('should set the BPP and depth to 4 and 3 respectively if in true color mode', function () { + client.set_true_color(true); + send_server_init({}, client); + expect(client._fb_Bpp).to.equal(4); + expect(client._fb_depth).to.equal(3); + }); + + it('should set the BPP and depth to 1 and 1 respectively if not in true color mode', function () { + client.set_true_color(false); + send_server_init({}, client); + expect(client._fb_Bpp).to.equal(1); + expect(client._fb_depth).to.equal(1); + }); + + // TODO(directxman12): test the various options in this configuration matrix + it('should reply with the pixel format, client encodings, and initial update request', function () { + client.set_true_color(true); + client.set_local_cursor(false); + // we skip the cursor encoding + var expected = {_sQ: new Uint8Array(34 + 4 * (client._encodings.length - 1)), _sQlen: 0}; + RFB.messages.pixelFormat(expected, 4, 3, true); + RFB.messages.clientEncodings(expected, client._encodings, false, true); + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, + dirtyBoxes: [ { x: 0, y: 0, w: 27, h: 32 } ] }; + RFB.messages.fbUpdateRequests(expected, expected_cdr, 27, 32); + + send_server_init({ width: 27, height: 32 }, client); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should transition to the "normal" state', function () { + send_server_init({}, client); + expect(client._rfb_state).to.equal('normal'); + }); + }); + }); + + describe('Protocol Message Processing After Completing Initialization', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + client._fb_width = 640; + client._fb_height = 20; + }); + + describe('Framebuffer Update Handling', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + client._fb_width = 640; + client._fb_height = 20; + }); + + var target_data_arr = [ + 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + ]; + var target_data; + + var target_data_check_arr = [ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]; + var target_data_check; + + before(function () { + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray + target_data = new Uint8Array(target_data_arr); + target_data_check = new Uint8Array(target_data_check_arr); + }); + + function send_fbu_msg (rect_info, rect_data, client, rect_cnt) { + var data = []; + + if (!rect_cnt || rect_cnt > -1) { + // header + data.push(0); // msg type + data.push(0); // padding + data.push16(rect_cnt || rect_data.length); + } + + for (var i = 0; i < rect_data.length; i++) { + if (rect_info[i]) { + data.push16(rect_info[i].x); + data.push16(rect_info[i].y); + data.push16(rect_info[i].width); + data.push16(rect_info[i].height); + data.push32(rect_info[i].encoding); + } + data = data.concat(rect_data[i]); + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + } + + it('should send an update request if there is sufficient data', function () { + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0}; + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, + dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; + RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); + + client._framebufferUpdate = function () { return true; }; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should not send an update request if we need more data', function () { + client._sock._websocket._receive_data(new Uint8Array([0])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + }); + + it('should resume receiving an update if we previously did not have enough data', function () { + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0}; + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, + dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; + RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); + + // just enough to set FBU.rects + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + + client._framebufferUpdate = function () { return true; }; // we magically have enough data + // 247 should *not* be used as the message type here + client._sock._websocket._receive_data(new Uint8Array([247])); + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should parse out information from a header before any actual data comes in', function () { + client.set_onFBUReceive(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02, encodingName: 'RRE' }; + send_fbu_msg([rect_info], [[]], client); + + var spy = client.get_onFBUReceive(); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(sinon.match.any, rect_info); + }); + + it('should fire onFBUComplete when the update is complete', function () { + client.set_onFBUComplete(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: -224, encodingName: 'last_rect' }; + send_fbu_msg([rect_info], [[]], client); // last_rect + + var spy = client.get_onFBUComplete(); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(sinon.match.any, rect_info); + }); + + it('should not fire onFBUComplete if we have not finished processing the update', function () { + client.set_onFBUComplete(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x00, encodingName: 'RAW' }; + send_fbu_msg([rect_info], [[]], client); + expect(client.get_onFBUComplete()).to.not.have.been.called; + }); + + it('should call the appropriate encoding handler', function () { + client._encHandlers[0x02] = sinon.spy(); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02 }; + send_fbu_msg([rect_info], [[]], client); + expect(client._encHandlers[0x02]).to.have.been.calledOnce; + }); + + it('should fail on an unsupported encoding', function () { + client.set_onFBUReceive(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; + send_fbu_msg([rect_info], [[]], client); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should be able to pause and resume receiving rects if not enought data', function () { + // seed some initial data to copy + client._fb_width = 4; + client._fb_height = 4; + client._display.resize(4, 4); + var initial_data = client._display._drawCtx.createImageData(4, 2); + var initial_data_arr = target_data_check_arr.slice(0, 32); + for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; } + client._display._drawCtx.putImageData(initial_data, 0, 0); + + var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, + { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; + // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }] + var rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg([info[0]], [rects[0]], client, 2); + send_fbu_msg([info[1]], [rects[1]], client, -1); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('Message Encoding Handlers', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display._fb_width = 4; + client._display._fb_height = 4; + client._display._viewportLoc.w = 4; + client._display._viewportLoc.h = 4; + client._fb_Bpp = 4; + }); + + it('should handle the RAW encoding', function () { + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + // data is in bgrx + var rects = [ + [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0], + [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0], + [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0], + [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle the COPYRECT encoding', function () { + // seed some initial data to copy + var initial_data = client._display._drawCtx.createImageData(4, 2); + var initial_data_arr = target_data_check_arr.slice(0, 32); + for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; } + client._display._drawCtx.putImageData(initial_data, 0, 0); + + var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, + { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; + // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }] + var rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data_check); + }); + + // TODO(directxman12): for encodings with subrects, test resuming on partial send? + // TODO(directxman12): test rre_chunk_sz (related to above about subrects)? + + it('should handle the RRE encoding', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }]; + var rect = []; + rect.push32(2); // 2 subrects + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push16(0); // x: 0 + rect.push16(0); // y: 0 + rect.push16(2); // width: 2 + rect.push16(2); // height: 2 + rect.push(0xff); // becomes ff0000ff --> #0000FF color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push16(2); // x: 2 + rect.push16(2); // y: 2 + rect.push16(2); // width: 2 + rect.push16(2); // height: 2 + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('the HEXTILE encoding handler', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display._fb_width = 4; + client._display._fb_height = 4; + client._display._viewportLoc.w = 4; + client._display._viewportLoc.h = 4; + client._fb_Bpp = 4; + }); + + it('should handle a tile with fg, bg specified, normal subrects', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rect = []; + rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(2); // 2 subrects + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push(2 | (2 << 4)); // x: 2, y: 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should handle a raw tile', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rect = []; + rect.push(0x01); // raw + for (var i = 0; i < target_data.length; i += 4) { + rect.push(target_data[i + 2]); + rect.push(target_data[i + 1]); + rect.push(target_data[i]); + rect.push(target_data[i + 3]); + } + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle a tile with only bg specified (solid bg)', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rect = []; + rect.push(0x02); + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + send_fbu_msg(info, [rect], client); + + var expected = []; + for (var i = 0; i < 16; i++) { expected.push32(0xff00ff); } + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should handle a tile with only bg specified and an empty frame afterwards', function () { + // set the width so we can have two tiles + client._fb_width = 8; + client._display._fb_width = 8; + client._display._viewportLoc.w = 8; + + var info = [{ x: 0, y: 0, width: 32, height: 4, encoding: 0x05 }]; + + var rect = []; + + // send a bg frame + rect.push(0x02); + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + + // send an empty frame + rect.push(0x00); + + send_fbu_msg(info, [rect], client); + + var expected = []; + var i; + for (i = 0; i < 16; i++) { expected.push32(0xff00ff); } // rect 1: solid + for (i = 0; i < 16; i++) { expected.push32(0xff00ff); } // rect 2: same bkground color + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should handle a tile with bg and coloured subrects', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rect = []; + rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(2); // 2 subrects + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(2 | (2 << 4)); // x: 2, y: 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should carry over fg and bg colors from the previous tile if not specified', function () { + client._fb_width = 4; + client._fb_height = 17; + client._display.resize(4, 17); + + var info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}]; + var rect = []; + rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(8); // 8 subrects + var i; + for (i = 0; i < 4; i++) { + rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + } + rect.push(0x08); // anysubrects + rect.push(1); // 1 subrect + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + + var expected = []; + for (i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); } + expected = expected.concat(target_data_check_arr.slice(0, 16)); + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should fail on an invalid subencoding', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rects = [[45]]; // an invalid subencoding + send_fbu_msg(info, rects, client); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + it.skip('should handle the TIGHT encoding', function () { + // TODO(directxman12): test this + }); + + it.skip('should handle the TIGHT_PNG encoding', function () { + // TODO(directxman12): test this + }); + + it('should handle the DesktopSize pseduo-encoding', function () { + client.set_onFBResize(sinon.spy()); + sinon.spy(client._display, 'resize'); + send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client); + + var spy = client.get_onFBResize(); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(sinon.match.any, 20, 50); + + expect(client._fb_width).to.equal(20); + expect(client._fb_height).to.equal(50); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(20, 50); + }); + + describe('the ExtendedDesktopSize pseudo-encoding handler', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + client._supportsSetDesktopSize = false; + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display._fb_width = 4; + client._display._fb_height = 4; + client._display._viewportLoc.w = 4; + client._display._viewportLoc.h = 4; + client._fb_Bpp = 4; + sinon.spy(client._display, 'resize'); + client.set_onFBResize(sinon.spy()); + }); + + function make_screen_data (nr_of_screens) { + var data = []; + data.push8(nr_of_screens); // number-of-screens + data.push8(0); // padding + data.push16(0); // padding + for (var i=0; i= rQlen + }); + + it('should call the error event handler on an exception', function () { + sock._eventHandlers.error = sinon.spy(); + sock._eventHandlers.message = sinon.stub().throws(); + var msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._eventHandlers.error).to.have.been.calledOnce; + }); + }); + + describe('Data encoding', function () { + before(function () { FakeWebSocket.replace(); }); + after(function () { FakeWebSocket.restore(); }); + + describe('as binary data', function () { + var sock; + beforeEach(function () { + sock = new Websock(); + sock.open('ws://', 'binary'); + sock._websocket._open(); + }); + + it('should only send the send queue up to the send queue length', function () { + sock._sQ = new Uint8Array([1, 2, 3, 4, 5]); + sock._sQlen = 3; + var res = sock._encode_message(); + expect(res).to.array.equal(new Uint8Array([1, 2, 3])); + }); + + it('should properly pass the encoded data off to the actual WebSocket', function () { + sock.send([1, 2, 3]); + expect(sock._websocket._get_sent_data()).to.array.equal(new Uint8Array([1, 2, 3])); + }); + }); + }); +}); diff --git a/public/novnc/tests/viewport.css b/public/novnc/tests/viewport.css new file mode 100644 index 00000000..86f65ff0 --- /dev/null +++ b/public/novnc/tests/viewport.css @@ -0,0 +1,43 @@ +html,body { + margin: 0px; + padding: 0px; + width: 100%; + height: 100%; +} + +.flex-layout { + width: 100%; + height: 100%; + + display: box; + display: -webkit-box; + display: -moz-box; + display: -ms-box; + + box-orient: vertical; + -webkit-box-orient: vertical; + -moz-box-orient: vertical; + -ms-box-orient: vertical; + + box-align: stretch; + -webkit-box-align: stretch; + -moz-box-align: stretch; + -ms-box-align: stretch; +} +.flex-box { + box-flex: 1; + -webkit-box-flex: 1; + -moz-box-flex: 1; + -ms-box-flex: 1; +} + +.container { + margin: 0px; + padding: 0px; +} + +.canvas { + position: absolute; + border-style: dotted; + border-width: 1px; +} diff --git a/public/novnc/tests/viewport.html b/public/novnc/tests/viewport.html new file mode 100644 index 00000000..8daff24e --- /dev/null +++ b/public/novnc/tests/viewport.html @@ -0,0 +1,203 @@ + + + Viewport Test + + + + + +
    +
    + Canvas: + +
    +
    +
    + + Canvas not supported. + +
    +
    +
    +
    + Results:
    + +
    +
    + + + + + + + + + + + + diff --git a/public/novnc/tests/vnc_perf.html b/public/novnc/tests/vnc_perf.html new file mode 100644 index 00000000..20c2e07d --- /dev/null +++ b/public/novnc/tests/vnc_perf.html @@ -0,0 +1,213 @@ + + + + VNC Performance Benchmark + + + + Passes:   + +   + +

    + + Results:
    + + +

    + +
    +
    + + +
    Loading
    +
    + + Canvas not supported. + +
    + + + + + + + + + + + diff --git a/public/novnc/tests/vnc_playback.html b/public/novnc/tests/vnc_playback.html new file mode 100644 index 00000000..2063f0c2 --- /dev/null +++ b/public/novnc/tests/vnc_playback.html @@ -0,0 +1,137 @@ + + + + VNC Playback + + + + Iterations:   + Perftest:  + Realtime:   + +   + +

    + + Results:
    + + +

    + +
    +
    + + +
    Loading
    +
    + + Canvas not supported. + +
    + + + + + + + + + + + diff --git a/public/novnc/utils/README.md b/public/novnc/utils/README.md new file mode 100644 index 00000000..344f199e --- /dev/null +++ b/public/novnc/utils/README.md @@ -0,0 +1,14 @@ +## WebSockets Proxy/Bridge + +Websockify has been forked out into its own project. `launch.sh` wil +automatically download it here if it is not already present and not +installed as system-wide. + +For more detailed description and usage information please refer to +the [websockify README](https://github.com/kanaka/websockify/blob/master/README.md). + +The other versions of websockify (C, Node.js) and the associated test +programs have been moved to +[websockify](https://github.com/kanaka/websockify). Websockify was +formerly named wsproxy. + diff --git a/public/novnc/utils/b64-to-binary.pl b/public/novnc/utils/b64-to-binary.pl new file mode 100644 index 00000000..280e28c9 --- /dev/null +++ b/public/novnc/utils/b64-to-binary.pl @@ -0,0 +1,17 @@ +#!/usr/bin/env perl +use MIME::Base64; + +for (<>) { + unless (/^'([{}])(\d+)\1(.+?)',$/) { + print; + next; + } + + my ($dir, $amt, $b64) = ($1, $2, $3); + + my $decoded = MIME::Base64::decode($b64) or die "Could not base64-decode line `$_`"; + + my $decoded_escaped = join "", map { "\\x$_" } unpack("(H2)*", $decoded); + + print "'${dir}${amt}${dir}${decoded_escaped}',\n"; +} diff --git a/public/novnc/utils/img2js.py b/public/novnc/utils/img2js.py new file mode 100644 index 00000000..4d213420 --- /dev/null +++ b/public/novnc/utils/img2js.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +# +# Convert image to Javascript compatible base64 Data URI +# Copyright 2011 Joel Martin +# Licensed under MPL 2.0 (see docs/LICENSE.MPL-2.0) +# + +import sys, base64 + +try: + from PIL import Image +except: + print "python PIL module required (python-imaging package)" + sys.exit(1) + + +if len(sys.argv) < 3: + print "Usage: %s IMAGE JS_VARIABLE" % sys.argv[0] + sys.exit(1) + +fname = sys.argv[1] +var = sys.argv[2] + +ext = fname.lower().split('.')[-1] +if ext == "png": mime = "image/png" +elif ext in ["jpg", "jpeg"]: mime = "image/jpeg" +elif ext == "gif": mime = "image/gif" +else: + print "Only PNG, JPEG and GIF images are supported" + sys.exit(1) +uri = "data:%s;base64," % mime + +im = Image.open(fname) +w, h = im.size + +raw = open(fname).read() + +print '%s = {"width": %s, "height": %s, "data": "%s%s"};' % ( + var, w, h, uri, base64.b64encode(raw)) diff --git a/public/novnc/utils/inflator.partial.js b/public/novnc/utils/inflator.partial.js new file mode 100644 index 00000000..8b6d1095 --- /dev/null +++ b/public/novnc/utils/inflator.partial.js @@ -0,0 +1,40 @@ +var zlib = require('../node_modules/pako/lib/zlib/inflate.js'); +var ZStream = require('../node_modules/pako/lib/zlib/zstream.js'); + +var Inflate = function () { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + zlib.inflateInit(this.strm, this.windowBits); +}; + +Inflate.prototype = { + inflate: function (data, flush, expected) { + this.strm.input = data; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.next_out = 0; + + // resize our output buffer if it's too small + // (we could just use multiple chunks, but that would cause an extra + // allocation each time to flatten the chunks) + if (expected > this.chunkSize) { + this.chunkSize = expected; + this.strm.output = new Uint8Array(this.chunkSize); + } + + this.strm.avail_out = this.chunkSize; + + zlib.inflate(this.strm, flush); + + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + }, + + reset: function () { + zlib.inflateReset(this.strm); + } +}; + +module.exports = {Inflate: Inflate}; diff --git a/public/novnc/utils/json2graph.py b/public/novnc/utils/json2graph.py new file mode 100644 index 00000000..f9ae27d3 --- /dev/null +++ b/public/novnc/utils/json2graph.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python + +''' +Use matplotlib to generate performance charts +Copyright 2011 Joel Martin +Licensed under MPL-2.0 (see docs/LICENSE.MPL-2.0) +''' + +# a bar plot with errorbars +import sys, json, pprint +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties + +def usage(): + print "%s json_file level1 level2 level3 [legend_height]\n\n" % sys.argv[0] + print "Description:\n" + print "level1, level2, and level3 are one each of the following:\n"; + print " select=ITEM - select only ITEM at this level"; + print " bar - each item on this level becomes a graph bar"; + print " group - items on this level become groups of bars"; + print "\n"; + print "json_file is a file containing json data in the following format:\n" + print ' {'; + print ' "conf": {'; + print ' "order_l1": ['; + print ' "level1_label1",'; + print ' "level1_label2",'; + print ' ...'; + print ' ],'; + print ' "order_l2": ['; + print ' "level2_label1",'; + print ' "level2_label2",'; + print ' ...'; + print ' ],'; + print ' "order_l3": ['; + print ' "level3_label1",'; + print ' "level3_label2",'; + print ' ...'; + print ' ]'; + print ' },'; + print ' "stats": {'; + print ' "level1_label1": {'; + print ' "level2_label1": {'; + print ' "level3_label1": [val1, val2, val3],'; + print ' "level3_label2": [val1, val2, val3],'; + print ' ...'; + print ' },'; + print ' "level2_label2": {'; + print ' ...'; + print ' },'; + print ' },'; + print ' "level1_label2": {'; + print ' ...'; + print ' },'; + print ' ...'; + print ' },'; + print ' }'; + sys.exit(2) + +def error(msg): + print msg + sys.exit(1) + + +#colors = ['#ff0000', '#0863e9', '#00f200', '#ffa100', +# '#800000', '#805100', '#013075', '#007900'] +colors = ['#ff0000', '#00ff00', '#0000ff', + '#dddd00', '#dd00dd', '#00dddd', + '#dd6622', '#dd2266', '#66dd22', + '#8844dd', '#44dd88', '#4488dd'] + +if len(sys.argv) < 5: + usage() + +filename = sys.argv[1] +L1 = sys.argv[2] +L2 = sys.argv[3] +L3 = sys.argv[4] +if len(sys.argv) > 5: + legendHeight = float(sys.argv[5]) +else: + legendHeight = 0.75 + +# Load the JSON data from the file +data = json.loads(file(filename).read()) +conf = data['conf'] +stats = data['stats'] + +# Sanity check data hierarchy +if len(conf['order_l1']) != len(stats.keys()): + error("conf.order_l1 does not match stats level 1") +for l1 in stats.keys(): + if len(conf['order_l2']) != len(stats[l1].keys()): + error("conf.order_l2 does not match stats level 2 for %s" % l1) + if conf['order_l1'].count(l1) < 1: + error("%s not found in conf.order_l1" % l1) + for l2 in stats[l1].keys(): + if len(conf['order_l3']) != len(stats[l1][l2].keys()): + error("conf.order_l3 does not match stats level 3") + if conf['order_l2'].count(l2) < 1: + error("%s not found in conf.order_l2" % l2) + for l3 in stats[l1][l2].keys(): + if conf['order_l3'].count(l3) < 1: + error("%s not found in conf.order_l3" % l3) + +# +# Generate the data based on the level specifications +# +bar_labels = None +group_labels = None +bar_vals = [] +bar_sdvs = [] +if L3.startswith("select="): + select_label = l3 = L3.split("=")[1] + bar_labels = conf['order_l1'] + group_labels = conf['order_l2'] + bar_vals = [[0]*len(group_labels) for i in bar_labels] + bar_sdvs = [[0]*len(group_labels) for i in bar_labels] + for b in range(len(bar_labels)): + l1 = bar_labels[b] + for g in range(len(group_labels)): + l2 = group_labels[g] + bar_vals[b][g] = np.mean(stats[l1][l2][l3]) + bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) +elif L2.startswith("select="): + select_label = l2 = L2.split("=")[1] + bar_labels = conf['order_l1'] + group_labels = conf['order_l3'] + bar_vals = [[0]*len(group_labels) for i in bar_labels] + bar_sdvs = [[0]*len(group_labels) for i in bar_labels] + for b in range(len(bar_labels)): + l1 = bar_labels[b] + for g in range(len(group_labels)): + l3 = group_labels[g] + bar_vals[b][g] = np.mean(stats[l1][l2][l3]) + bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) +elif L1.startswith("select="): + select_label = l1 = L1.split("=")[1] + bar_labels = conf['order_l2'] + group_labels = conf['order_l3'] + bar_vals = [[0]*len(group_labels) for i in bar_labels] + bar_sdvs = [[0]*len(group_labels) for i in bar_labels] + for b in range(len(bar_labels)): + l2 = bar_labels[b] + for g in range(len(group_labels)): + l3 = group_labels[g] + bar_vals[b][g] = np.mean(stats[l1][l2][l3]) + bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) +else: + usage() + +# If group is before bar then flip (zip) the data +if [L1, L2, L3].index("group") < [L1, L2, L3].index("bar"): + bar_labels, group_labels = group_labels, bar_labels + bar_vals = zip(*bar_vals) + bar_sdvs = zip(*bar_sdvs) + +print "bar_vals:", bar_vals + +# +# Now render the bar graph +# +ind = np.arange(len(group_labels)) # the x locations for the groups +width = 0.8 * (1.0/len(bar_labels)) # the width of the bars + +fig = plt.figure(figsize=(10,6), dpi=80) +plot = fig.add_subplot(1, 1, 1) + +rects = [] +for i in range(len(bar_vals)): + rects.append(plot.bar(ind+width*i, bar_vals[i], width, color=colors[i], + yerr=bar_sdvs[i], align='center')) + +# add some +plot.set_ylabel('Milliseconds (less is better)') +plot.set_title("Javascript array test: %s" % select_label) +plot.set_xticks(ind+width) +plot.set_xticklabels( group_labels ) + +fontP = FontProperties() +fontP.set_size('small') +plot.legend( [r[0] for r in rects], bar_labels, prop=fontP, + loc = 'center right', bbox_to_anchor = (1.0, legendHeight)) + +def autolabel(rects): + # attach some text labels + for rect in rects: + height = rect.get_height() + if np.isnan(height): + height = 0.0 + plot.text(rect.get_x()+rect.get_width()/2., height+20, '%d'%int(height), + ha='center', va='bottom', size='7') + +for rect in rects: + autolabel(rect) + +# Adjust axis sizes +axis = list(plot.axis()) +axis[0] = -width # Make sure left side has enough for bar +#axis[1] = axis[1] * 1.20 # Add 20% to the right to make sure it fits +axis[2] = 0 # Make y-axis start at 0 +axis[3] = axis[3] * 1.10 # Add 10% to the top +plot.axis(axis) + +plt.show() diff --git a/public/novnc/utils/launch.sh b/public/novnc/utils/launch.sh new file mode 100644 index 00000000..ce30aa38 --- /dev/null +++ b/public/novnc/utils/launch.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash + +# Copyright 2016 Joel Martin +# Copyright 2016 Solly Ross +# Licensed under MPL 2.0 or any later version (see LICENSE.txt) + +usage() { + if [ "$*" ]; then + echo "$*" + echo + fi + echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT]" + echo + echo "Starts the WebSockets proxy and a mini-webserver and " + echo "provides a cut-and-paste URL to go to." + echo + echo " --listen PORT Port for proxy/webserver to listen on" + echo " Default: 6080" + echo " --vnc VNC_HOST:PORT VNC server host:port proxy target" + echo " Default: localhost:5900" + echo " --cert CERT Path to combined cert/key file" + echo " Default: self.pem" + echo " --web WEB Path to web files (e.g. vnc.html)" + echo " Default: ./" + exit 2 +} + +NAME="$(basename $0)" +REAL_NAME="$(readlink -f $0)" +HERE="$(cd "$(dirname "$REAL_NAME")" && pwd)" +PORT="6080" +VNC_DEST="localhost:5900" +CERT="" +WEB="" +proxy_pid="" + +die() { + echo "$*" + exit 1 +} + +cleanup() { + trap - TERM QUIT INT EXIT + trap "true" CHLD # Ignore cleanup messages + echo + if [ -n "${proxy_pid}" ]; then + echo "Terminating WebSockets proxy (${proxy_pid})" + kill ${proxy_pid} + fi +} + +# Process Arguments + +# Arguments that only apply to chrooter itself +while [ "$*" ]; do + param=$1; shift; OPTARG=$1 + case $param in + --listen) PORT="${OPTARG}"; shift ;; + --vnc) VNC_DEST="${OPTARG}"; shift ;; + --cert) CERT="${OPTARG}"; shift ;; + --web) WEB="${OPTARG}"; shift ;; + -h|--help) usage ;; + -*) usage "Unknown chrooter option: ${param}" ;; + *) break ;; + esac +done + +# Sanity checks +which netstat >/dev/null 2>&1 \ + || die "Must have netstat installed" + +netstat -a -p tcp -n | grep -qs "${PORT} .*LISTEN" \ + && die "Port ${PORT} in use. Try --listen PORT" + +trap "cleanup" TERM QUIT INT EXIT + +# Find vnc.html +if [ -n "${WEB}" ]; then + if [ ! -e "${WEB}/vnc.html" ]; then + die "Could not find ${WEB}/vnc.html" + fi +elif [ -e "$(pwd)/vnc.html" ]; then + WEB=$(pwd) +elif [ -e "${HERE}/../vnc.html" ]; then + WEB=${HERE}/../ +elif [ -e "${HERE}/vnc.html" ]; then + WEB=${HERE} +elif [ -e "${HERE}/../share/novnc/vnc.html" ]; then + WEB=${HERE}/../share/novnc/ +else + die "Could not find vnc.html" +fi + +# Find self.pem +if [ -n "${CERT}" ]; then + if [ ! -e "${CERT}" ]; then + die "Could not find ${CERT}" + fi +elif [ -e "$(pwd)/self.pem" ]; then + CERT="$(pwd)/self.pem" +elif [ -e "${HERE}/../self.pem" ]; then + CERT="${HERE}/../self.pem" +elif [ -e "${HERE}/self.pem" ]; then + CERT="${HERE}/self.pem" +else + echo "Warning: could not find self.pem" +fi + +# try to find websockify (prefer local, try global, then download local) +if [[ -e ${HERE}/websockify ]]; then + WEBSOCKIFY=${HERE}/websockify/run + + if [[ ! -x $WEBSOCKIFY ]]; then + echo "The path ${HERE}/websockify exists, but $WEBSOCKIFY either does not exist or is not executable." + echo "If you intended to use an installed websockify package, please remove ${HERE}/websockify." + exit 1 + fi + + echo "Using local websockify at $WEBSOCKIFY" +else + WEBSOCKIFY=$(which websockify 2>/dev/null) + + if [[ $? -ne 0 ]]; then + echo "No installed websockify, attempting to clone websockify..." + WEBSOCKIFY=${HERE}/websockify/run + git clone https://github.com/kanaka/websockify ${HERE}/websockify + + if [[ ! -e $WEBSOCKIFY ]]; then + echo "Unable to locate ${HERE}/websockify/run after downloading" + exit 1 + fi + + echo "Using local websockify at $WEBSOCKIFY" + else + echo "Using installed websockify at $WEBSOCKIFY" + fi +fi + +echo "Starting webserver and WebSockets proxy on port ${PORT}" +#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & +${WEBSOCKIFY} --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & +proxy_pid="$!" +sleep 1 +if ! ps -p ${proxy_pid} >/dev/null; then + proxy_pid= + echo "Failed to start WebSockets proxy" + exit 1 +fi + +echo -e "\n\nNavigate to this URL:\n" +echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" +echo -e "Press Ctrl-C to exit\n\n" + +wait ${proxy_pid} diff --git a/public/novnc/utils/parse.js b/public/novnc/utils/parse.js new file mode 100644 index 00000000..02ac66c2 --- /dev/null +++ b/public/novnc/utils/parse.js @@ -0,0 +1,97 @@ +// Utility to parse keysymdef.h to produce mappings from Unicode codepoints to keysyms +"use strict"; + +var fs = require('fs'); + +var show_help = process.argv.length === 2; +var use_keynames = false; +var filename; + +for (var i = 2; i < process.argv.length; ++i) { + switch (process.argv[i]) { + case "--help": + case "-h": + show_help = true; + break; + case "--debug-names": + case "-d": + use_keynames = true; + break; + case "--file": + case "-f": + default: + filename = process.argv[i]; + } +} + +if (!filename) { + show_help = true; + console.log("Error: No filename specified\n"); +} + +if (show_help) { + console.log("Parses a *nix keysymdef.h to generate Unicode code point mappings"); + console.log("Usage: node parse.js [options] filename:"); + console.log(" -h [ --help ] Produce this help message"); + console.log(" -d [ --debug-names ] Preserve keysym names for debugging (Increases file size by ~40KB)"); + console.log(" filename The keysymdef.h file to parse"); + return; +} + +// Set this to false to omit key names from the generated keysymdef.js +// This reduces the file size by around 40kb, but may hinder debugging + +var buf = fs.readFileSync(filename); +var str = buf.toString('utf8'); + +var re = /^\#define XK_([a-zA-Z_0-9]+)\s+0x([0-9a-fA-F]+)\s*(\/\*\s*(.*)\s*\*\/)?\s*$/m; + +var arr = str.split('\n'); + +var keysyms = {}; +var codepoints = {}; + +for (var i = 0; i < arr.length; ++i) { + var result = re.exec(arr[i]); + if (result){ + var keyname = result[1]; + var keysym = parseInt(result[2], 16); + var remainder = result[3]; + + keysyms[keysym] = keyname; + + var unicodeRes = /U\+([0-9a-fA-F]+)/.exec(remainder); + if (unicodeRes) { + var unicode = parseInt(unicodeRes[1], 16); + if (!codepoints[unicode]){ + codepoints[unicode] = keysym; + } + } + else { + console.log("no unicode codepoint found:", arr[i]); + } + } + else { + console.log("line is not a keysym:", arr[i]); + } +} + +var out = "// This file describes mappings from Unicode codepoints to the keysym values\n" + +"// (and optionally, key names) expected by the RFB protocol\n" + +"// How this file was generated:\n" + +"// " + process.argv.join(" ") + "\n" + +"var keysyms = (function(){\n" + +" \"use strict\";\n" + +" var keynames = {keysyms};\n" + +" var codepoints = {codepoints};\n" + +"\n" + +" function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; }\n" + +" return {\n" + +" fromUnicode : function(u) { return lookup(codepoints[u]); },\n" + +" lookup : lookup\n" + +" };\n" + +"})();\n"; +out = out.replace('{keysyms}', use_keynames ? JSON.stringify(keysyms) : "null"); +out = out.replace('{codepoints}', JSON.stringify(codepoints)); + +fs.writeFileSync("keysymdef.js", out); diff --git a/public/novnc/utils/u2x11 b/public/novnc/utils/u2x11 new file mode 100644 index 00000000..fd3e4ba8 --- /dev/null +++ b/public/novnc/utils/u2x11 @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Convert "U+..." commented entries in /usr/include/X11/keysymdef.h +# into JavaScript for use by noVNC. Note this is likely to produce +# a few duplicate properties with clashing values, that will need +# resolving manually. +# +# Colin Dean +# + +regex="^#define[ \t]+XK_[A-Za-z0-9_]+[ \t]+0x([0-9a-fA-F]+)[ \t]+\/\*[ \t]+U\+([0-9a-fA-F]+)[ \t]+[^*]+.[ \t]+\*\/[ \t]*$" +echo "unicodeTable = {" +while read line; do + if echo "${line}" | egrep -qs "${regex}"; then + + x11=$(echo "${line}" | sed -r "s/${regex}/\1/") + vnc=$(echo "${line}" | sed -r "s/${regex}/\2/") + + if echo "${vnc}" | egrep -qs "^00[2-9A-F][0-9A-F]$"; then + : # skip ISO Latin-1 (U+0020 to U+00FF) as 1-to-1 mapping + else + # note 1-to-1 is possible (e.g. for Euro symbol, U+20AC) + echo " 0x${vnc} : 0x${x11}," + fi + fi +done < /usr/include/X11/keysymdef.h | uniq +echo "};" + diff --git a/public/novnc/utils/websockify b/public/novnc/utils/websockify new file mode 160000 index 00000000..f0bdb0a6 --- /dev/null +++ b/public/novnc/utils/websockify @@ -0,0 +1 @@ +Subproject commit f0bdb0a621a4f3fb328d1410adfeaff76f088bfd diff --git a/public/novnc/vnc.html b/public/novnc/vnc.html new file mode 100644 index 00000000..0b653065 --- /dev/null +++ b/public/novnc/vnc.html @@ -0,0 +1,225 @@ + + + + + + noVNC + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    + + + + + + + +
    + + + + + +
    +
    +
    + +
    + + +
    + + + + + + + +
    + + + +
    + noVNC is a browser based VNC client implemented using HTML5 Canvas + and WebSockets. You will either need a VNC server with WebSockets + support (such as libvncserver) + or you will need to use + websockify + to bridge between your browser and VNC server. See the noVNC + README + and website + for more information. +
    + +
    + + +
    +
    + + +
    + +
    + +
    + + +
    + + + + + +
    + + +
    + +
      +
    • Encrypt
    • +
    • True Color
    • +
    • Local Cursor
    • +
    • Clip to Window
    • +
    • Shared Mode
    • +
    • View Only
    • +
      +
    • Path
    • +
    • +
    • +
    • Repeater ID
    • +
      + +
    • +
    • + + +
    • +
    • +
      +
    • +
    +
    +
    + + +
    +
      +
    • +
    • +
    • +
    • +
    • +
    +
    + +
    + + +
    +

    no
    VNC

    + + +
    + + Canvas not supported. + +
    + +
    + + + + + diff --git a/public/novnc/vnc_auto.html b/public/novnc/vnc_auto.html new file mode 100644 index 00000000..878d05a8 --- /dev/null +++ b/public/novnc/vnc_auto.html @@ -0,0 +1,247 @@ + + + + + + noVNC + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + +
    + Loading +
    + + + + + + +
    +
    + + Canvas not supported. + +
    + + + + + diff --git a/public/pages/bhyvevms/bhyveslist.table b/public/pages/bhyvevms/bhyveslist.table index 022c3faa..867d5467 100644 --- a/public/pages/bhyvevms/bhyveslist.table +++ b/public/pages/bhyvevms/bhyveslist.table @@ -2,7 +2,7 @@ #node# #jname# - + #vm_ram# #vm_cpus# #vm_os_type# diff --git a/public/pages/bhyvevms/en.index.php b/public/pages/bhyvevms/en.index.php index 0b7fa677..e299c7bb 100644 --- a/public/pages/bhyvevms/en.index.php +++ b/public/pages/bhyvevms/en.index.php @@ -18,6 +18,7 @@ $clonos->useDialogs(array( Node name VM + Usage RAM CPU OS type diff --git a/public/pages/jailscontainers/en.index.php b/public/pages/jailscontainers/en.index.php index 23e91062..e8000657 100644 --- a/public/pages/jailscontainers/en.index.php +++ b/public/pages/jailscontainers/en.index.php @@ -15,6 +15,7 @@ $clonos->useDialogs(array( Node name Jail + Usage IP-address Status Action diff --git a/public/pages/jailscontainers/jailslist.table b/public/pages/jailscontainers/jailslist.table index b92de385..fdb9717f 100644 --- a/public/pages/jailscontainers/jailslist.table +++ b/public/pages/jailscontainers/jailslist.table @@ -2,7 +2,7 @@ #node# #jname# - + #ip4_addr# #jstatus# diff --git a/public/pages/overview/ru.index.php b/public/pages/overview/ru.index.php index c9efab16..620ffc2f 100644 --- a/public/pages/overview/ru.index.php +++ b/public/pages/overview/ru.index.php @@ -1,25 +1,37 @@

    Общая статистика облака:

    - - - - - - - - - - - - - - - - - - - -
    ПараметрЗначение
    Количество нод:
    Нод онлайн:
    Нод офлайн:
    Количество клеток:
    Количество ядер:
    Средняя частота, Mhz:
    Всего RAM:
    Объём хранилища:
    +
    +
    + + + + + + + + + + + + + + + + + + + +
    ПараметрЗначение
    Количество нод:
    Нод онлайн:
    Нод офлайн:
    Количество клеток:
    Количество ядер:
    Средняя частота, Mhz:
    Всего RAM:
    Объём хранилища:
    +
    +
    +

    Зарузка процессоров в кластере:

    +
    + +

    Использование памяти в кластере:

    +
    +
    +
    +

    Это открытый и свободный проект, использующий в своей работе такие проекты, как (наиболее значимые):

    • CBSD Project — Фреймворк для управления виртуальными окружениями FreeBSD ОС