├── .github └── workflows │ └── webapp.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_OLD.md ├── gulpfile.js ├── html-minifier.conf ├── package-lock.json ├── package.json └── src ├── html ├── donate.html ├── dot.gif ├── faq.html ├── guacamole.min.js ├── index.html ├── main.css ├── news.html ├── notify.m4a ├── notify.mp3 ├── notify.ogg ├── rules.html └── test.png ├── js ├── collab-vm │ ├── collab-vm.js │ ├── common.js │ ├── en-us-qwerty.js │ └── jquery.history.js └── guacamole │ ├── ArrayBufferReader.js │ ├── ArrayBufferWriter.js │ ├── AudioChannel.js │ ├── BlobReader.js │ ├── Client.js │ ├── Display.js │ ├── InputStream.js │ ├── IntegerPool.js │ ├── Keyboard.js │ ├── Layer.js │ ├── Mouse.js │ ├── OnScreenKeyboard.js │ ├── OutputStream.js │ ├── Parser.js │ ├── Status.js │ ├── StringReader.js │ ├── StringWriter.js │ ├── Tunnel.js │ └── Version.js ├── res ├── dot.gif ├── main.css ├── notify.m4a ├── notify.mp3 └── notify.ogg └── templates ├── navbar.html └── win-logo-ascii.html /.github/workflows/webapp.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: "[collab-vm-web-app] - Node.js Gulp" 5 | 6 | on: 7 | push: 8 | branches: [ master, semantic ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | check_skip: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | skip: ${{ steps.result_step.outputs.result }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - uses: mstachniuk/ci-skip@master 22 | with: 23 | commit-filter: '[skip ci];[ci skip];[no ci]' 24 | commit-filter-separator: ';' 25 | - run: echo "::set-output name=result::${CI_SKIP}" 26 | id: result_step 27 | 28 | gulp: 29 | needs: check_skip 30 | if: ${{ needs.check_skip.outputs.skip == 'false' }} 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | node-version: [10.x, 12.x, 14.x] 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Use Node.js ${{ matrix.node-version }} 38 | uses: actions/setup-node@v1 39 | with: 40 | node-version: ${{ matrix.node-version }} 41 | - run: npm install --global gulp-cli 42 | - run: npm install 43 | - run: gulp 44 | - name: Upload Artifacts 45 | uses: 'actions/upload-artifact@v2' 46 | with: 47 | name: Web App built by Node ${{ matrix.node-version }} 48 | path: './build/*' 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/**/* 2 | node_modules/**/* 3 | build.* 4 | 5 | # HISTORIC old drop that the webapp doesn't use 6 | src/html/guacamole.min.js 7 | tmp/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # collab-vm-classic-web-app 2 | 3 | This is the archived source code of the original CollabVM webapp. It is no longer maintained by computernewb and is not recommended for use. 4 | 5 | For the current webapp used at /collab-vm/, see [collab-vm-1.2-webapp](https://github.com/computernewb/collab-vm-1.2-webapp/) 6 | 7 | For the maintained fork of this webapp used at /collab-vm/classic/, see [yellows111/collab-vm-web-app](https://github.com/yellows111/collab-vm-web-app/) -------------------------------------------------------------------------------- /README_OLD.md: -------------------------------------------------------------------------------- 1 | ![[collab-vm-web-app] - Node.js Gulp](https://github.com/yellows111/collab-vm-web-app/workflows/%5Bcollab-vm-web-app%5D%20-%20Node.js%20Gulp/badge.svg) 2 | 3 | # collab-vm-web-app 4 | The Collab VM Web App powers the website-portion of Collab VM and the Virus Farm. 5 | 6 | # What it contains 7 | * The scripts that power CollabVM. 8 | 9 | # Compilation requirements 10 | To compile this, you need a *nix or a Windows-like environment. This has only been compiled on x64 (amd64), but it should work on other architectures (like x86 or arm). You need a system that supports node.js and npm. node.js and npm run on Windows, Mac OS X, virtually any Linux distribution, FreeBSD, OpenBSD, Android, NetBSD, OpenSUSE, SmartOS, WebOS, NonStop OS, and other operating systems. (However, compilation has only been tested on x64 Windows 7 and x64 Ubuntu 14.04 and 16.04). 11 | 12 | That's basically all the requirements needed. 13 | 14 | # Compilation instructions 15 | 16 | To build the web app, follow these steps. 17 | 18 | * Step 1: Install node.js and npm. Windows and Mac OS X installers can be found here: https://nodejs.org/en/download/. For Linux, BSD, and other operating systems, visit this page: https://nodejs.org/en/download/package-manager/. 19 | * Step 2: Make sure npm is updated fully by typing npm install --global npm in the terminal. 20 | * Step 3: Install gulp from npm. You can do this by doing: npm install --global gulp-cli 21 | * Step 4: Open the web-app directory and type: npm install 22 | * Step 5: (Optionally) change the settings in common.js or set some of the DEBUG settings to true in collab-vm.js 23 | * Step 6: If you're testing this locally, you need to set DEBUG = true in collab-vm.js and do: gulp guacamole. (If you're not testing this locally, this isn't required.) 24 | * Step 7: Type gulp (or you can compile them indivdiually by typing gulp js, gulp html, etc). 25 | 26 | # License 27 | Collab VM Web App, as well as Collab VM Admin Web App and Collab VM Server are licensed under the [Apache License](https://www.apache.org/licenses/LICENSE-2.0). 28 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'), 2 | rename = require('gulp-rename'), 3 | concat = require('gulp-concat'), 4 | inject = require('gulp-inject'), 5 | htmlmin = require('gulp-htmlmin'), 6 | terser = require('gulp-terser'), 7 | rollup = require('gulp-rollup'), 8 | gap = require('gulp-append-prepend') 9 | sourcemaps = require('gulp-sourcemaps') 10 | fs = require('fs'); 11 | 12 | // Paths 13 | const paths = { 14 | html: "src/html/*.html", 15 | 16 | // Guacamole code. 17 | // This is bundleified using a abomination 18 | guacamole: [ 19 | 'src/js/guacamole/Display.js', 20 | 'src/js/guacamole/Parser.js', 21 | 'src/js/guacamole/Client.js', 22 | 'src/js/guacamole/OnScreenKeyboard.js', 23 | 'src/js/guacamole/ArrayBufferReader.js', 24 | 'src/js/guacamole/OutputStream.js', 25 | 'src/js/guacamole/BlobReader.js', 26 | 'src/js/guacamole/StringReader.js', 27 | 'src/js/guacamole/Mouse.js', 28 | 'src/js/guacamole/Tunnel.js', 29 | 'src/js/guacamole/AudioChannel.js', 30 | 'src/js/guacamole/InputStream.js', 31 | 'src/js/guacamole/IntegerPool.js', 32 | 'src/js/guacamole/Keyboard.js', 33 | 'src/js/guacamole/Status.js', 34 | 'src/js/guacamole/ArrayBufferWriter.js', 35 | 'src/js/guacamole/Layer.js', 36 | 'src/js/guacamole/StringWriter.js', 37 | 'src/js/guacamole/Version.js' 38 | ], 39 | 40 | // It's important to note that these file paths are 41 | // in a virtual filesystem to rollup. Add any files here which 42 | // rollup needs to see. 43 | client: [ 44 | 'tmp/guacamole.module.js', 45 | 'src/js/collab-vm/common.js', 46 | 'src/js/collab-vm/en-us-qwerty.js', 47 | 'src/js/collab-vm/collab-vm.js', 48 | 49 | 'src/js/collab-vm/jquery.history.js', 50 | ], 51 | 52 | client_entry: 'src/js/collab-vm/collab-vm.js' 53 | }; 54 | 55 | function HtmlTask() { 56 | return gulp.src([paths.html]) 57 | .pipe(inject(gulp.src(['build/all.min.js', 'build/*.css'], { read: false }), { ignorePath: 'build', addRootSlash: false })) 58 | .pipe(inject(gulp.src('src/templates/*.html').pipe(rename((path) => { 59 | // This is a trick to use gulp-inject as a simple html template engine 60 | // It switches the filename and extension so multiple templates can be used 61 | path.extname = '.' + path.basename; 62 | path.basename = 'html'; 63 | })), { 64 | transform: function (filePath, file) { 65 | return file.contents.toString('utf8'); 66 | } 67 | })) 68 | .pipe(htmlmin(JSON.parse(fs.readFileSync('html-minifier.conf', 'utf8')))) 69 | .pipe(gulp.dest('build', { overwrite: true })); 70 | } 71 | 72 | function CompressBundle() { 73 | return gulp.src('tmp/all.js') 74 | .pipe(sourcemaps.init()) 75 | .pipe(terser({ 76 | output: { 77 | comments: false 78 | }, 79 | mangle: { 80 | eval: true, 81 | module: true, 82 | toplevel: true 83 | } 84 | })) 85 | .pipe(concat('all.min.js')) 86 | .pipe(sourcemaps.write('.')) 87 | .pipe(gulp.dest('build')); 88 | } 89 | 90 | function BuildBundle() { 91 | return gulp.src(paths.client) 92 | .pipe(sourcemaps.init()) 93 | .pipe(rollup({ 94 | input: paths.client_entry, 95 | output: { 96 | format: "iife", 97 | name: "e" // unused but we need it for IIFE/closure 98 | } 99 | })) 100 | .pipe(concat('all.js')) 101 | .pipe(sourcemaps.write('.')) 102 | .pipe(gulp.dest('tmp')); 103 | } 104 | 105 | function ResTask() { 106 | return gulp.src('src/res/**/*') 107 | .pipe(gulp.dest('build')); 108 | } 109 | 110 | 111 | // This task throws all of the Guacamole code together into a temporary file 112 | // which we later use to build the module. 113 | function ConcatGuacModule() { 114 | return gulp.src(paths.guacamole) 115 | .pipe(concat('guacamole.module.tmp.js')) 116 | .pipe(gulp.dest('tmp')); // todo... 117 | } 118 | 119 | // This task then takes the temporary file from the ConcatGuacModule step, 120 | // and wraps it with the code needed to turn it into a legitimate ES6 module. 121 | // After we do this, we now have code that can be integrated into some module space. 122 | function BuildGuacModule() { 123 | return gulp.src('tmp/guacamole.module.tmp.js') 124 | .pipe(gap.prependText('export function GetGuacamole() {')) 125 | .pipe(gap.appendText('return Guacamole; }')) 126 | .pipe(concat('guacamole.module.js')) 127 | .pipe(gulp.dest('tmp')); // todo... 128 | } 129 | 130 | 131 | 132 | // default task 133 | exports.default = gulp.series( 134 | ConcatGuacModule, // These two need to be done first before the client bundle. 135 | BuildGuacModule, 136 | 137 | BuildBundle, 138 | CompressBundle, // makes prod bundle 139 | ResTask, 140 | HtmlTask 141 | ); 142 | -------------------------------------------------------------------------------- /html-minifier.conf: -------------------------------------------------------------------------------- 1 | { 2 | "removeComments": true, 3 | "removeCommentsFromCDATA": true, 4 | "removeCDATASectionsFromCDATA": true, 5 | "collapseWhitespace": true, 6 | "conservativeCollapse": false, 7 | "collapseBooleanAttributes": false, 8 | "removeAttributeQuotes": false, 9 | "removeRedundantAttributes": false, 10 | "useShortDoctype": true, 11 | "removeEmptyAttributes": false, 12 | "removeOptionalTags": false, 13 | "removeEmptyElements": false, 14 | "lint": false, 15 | "keepClosingSlash": false, 16 | "caseSensitive": false, 17 | "minifyJS": false, 18 | "minifyCSS": true, 19 | "ignoreCustomComments": [], 20 | "processScripts": [] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collab-vm-web-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "gulp", 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "Dartz & Cosmic Sans", 9 | "license": "Apache-2.0", 10 | "devDependencies": { 11 | "gulp": "^4.0.2", 12 | "gulp-concat": "^2.6.1", 13 | "gulp-htmlmin": "^5.0.1", 14 | "gulp-inject": "^5.0.4", 15 | "gulp-rename": "^1.4.0", 16 | "gulp-sourcemaps": "^2.6.5", 17 | "gulp-terser": "^1.2.0" 18 | }, 19 | "dependencies": { 20 | "gulp-append-prepend": "^1.0.8", 21 | "gulp-rollup": "^2.17.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/html/donate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | collab-vm Donations 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 37 | 38 |
39 |

Donations


40 |
A few people asked me to put up a donate button, so here you go. Do you like Collab VM? Do you want to donate to the developers of the project? You can do so by clicking the button below. Please note that Collab VM is a completely free service! You have no obligation to donate even if you use the virtual machine a lot. Just going to the website is enough support. Only donate if you really want.

Soon there will be donor features, which will include longer turns, a colored name, and other things.

41 | 42 | 44 | 45 | 46 |

All donations are done from PayPal. At this time, I unfortunately cannot take mailed cash, checks, BitCoin, money transfers, or anything like that. Sorry. 47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /src/html/dot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/html/dot.gif -------------------------------------------------------------------------------- /src/html/faq.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | collab-vm Donations 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 37 | 38 |
39 |

Frequently Asked Questions

40 |
Here are some frequently asked questions that I receive about CollabVM. 41 |

Q1. What is CollabVM?

42 |
A. CollabVM is a website that lets you control a virtualized Windows 7 machine via your web browser, for fun, testing, and overall entertainment. 43 |

Q2. How do I use this website?

44 |
A. Just click on the screen and wait for your turn. If there are no turns you will be given one automatically. 45 |

Q3. The VM is really messed up, slow, or Windows will no longer start, now what?

46 |
A. If the VM will no longer start or is really messed up, you should click the "Vote for Reset" button to vote for a reset. If it succeeds, this will reset the VM to a clean, non-messed up state. If the VM really needs a manual reset or the vote reset is broken email me at rightowner@gmail.com. 47 |

Q4. Some names are blue or yellow or both, what does that mean?

48 |
A. A blue name means that person is the one who is currently in control of the virtual machine. Anybody waiting for a turn will receive a yellow name. If they have not requested a turn yet, they won't have any color. 49 |

Q5. What happens if the computer is shut down?

50 |
A. I have made it so turning off the computer is pretty hard. If it is bypassed however nothing happens, it just restarts even if "Shut Down" is clicked. 51 |

Q6. What are the specifications of the VM?

52 |
A. 1.5 gigabytes of RAM, 50 GB hard drive, QEMU64 CPU @ 2 GHz, and a QXL video card capable of supporting 2D graphics. 53 |

Q7. Why Windows 7? Why not Mac OS X, Linux, BSD, etc?

54 |
A. Windows was just the best choice for something like this. In the future however, there will be a wider variation of virtual machines. 55 |

Q8. Why won't CollabVM load for me? What's wrong?

56 |
A. If it doesn't load, please make sure you do not have any of the following addons installed:

57 | NoScript

58 | uMatrix

59 | Ghostery is also known to affect some users of CollabVM

60 | If you have any of these addons installed, make an exception for CollabVM in them and try connecting again.

61 | If you don't have any of those add-ons installed, the servers may be down. Please contact me at rightowner@gmail.com.

62 | An unlikely sceneraio is that you may be banned from CollabVM. To test this, you can use a VPN/proxy to connect. (If you are banned, please do not use a VPN/Proxy to control the virtual machine unless you have my explicit permission. Only do this to check if you are banned.)

63 | If you are banned, please contact me and we can see if we can sort it out.
64 |

Q9. What exactly is "off-limits"? Can I be banned?

65 |
A. It is possible to be banned, although it's pretty difficult to do so. Read the rules if you are concerned about them. 66 |

Q10. It's asking me to login to the computer. What is the password?

67 |
A. By default, there is no password on the computer, not on the Administrator account, nor the default "Dartz" account. If there is one, somebody put it there. If you cannot login just email me at rightowner@gmail.com and I'll fix it. 68 |

Q11. Who coded and designed CollabVM?

69 |
A. CollabVM was written by Cosmic Sans. The site was designed by Cosmic Sans & Dartz (me). 70 |

Q12. It's stuck at a blank screen and on the top left it says "serial console" or "parallel console", help?

71 |
A. Press CTRL + ALT + 1 to get out of it. 72 |

Q13. The keyboard is stuck or is opening shortcuts, help?

73 |
A. Press CTRL and ALT at the same time on your physical keyboard, this should unstick the keys. If Windows key is held down, press that as well. 74 |

Q14. Is it safe to view this at work, school, etc?

75 |
A. Sometimes it is, sometimes it isn't. Before you can connect, the computer screen is censored until you click continue, but it's probably best to avoid connecting from an office place, as anyone can do anything, including opening something NSFW (e.g. porn) or even setting it as the wallpaper, for an example. 76 |

Q15. Will I break the VM if I go to http://computernewb.com/collab-vm/ on it?

77 |
A. No, the VM will continue to operate as if nothing is happening, even if you request a turn it will be fine (although the cursor will just stay there during its turn). 78 |

Q16. How is this all hosted?

79 |
A. This VM is hosted from a LunaNode KVM VPS server with 4 GB of RAM, using a customly built HTML5 Guacamole. 80 |

Q17. Is the project open source?

81 |
A. Unfortunately at this time the project is not open source, but there are plans to release its source code in the future when more time can be made. 82 |
83 |
84 |
85 |
86 |
87 | 88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /src/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Control Collaborative Virtual Machines! 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 49 | 50 | 72 | 91 | 113 |
114 | 132 | 139 | 171 | 261 |
262 | 263 | 264 | -------------------------------------------------------------------------------- /src/html/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | body { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | .guac-hide-cursor { 10 | cursor: url('dot.gif'),default; 11 | } 12 | .focused { 13 | box-shadow: 0px 0px 9px 0px rgba(45, 213, 255, 0.75); 14 | -moz-box-shadow: 0px 0px 9px 0px rgba(45, 213, 255, 0.75); 15 | -webkit-box-shadow: 0px 0px 9px 0px rgba(45, 213, 255, 0.75); 16 | } 17 | .waiting { 18 | box-shadow: 0px 0px 9px 0px rgba(242, 255, 63, 0.75); 19 | -moz-box-shadow: 0px 0px 9px 0px rgba(242, 255, 63, 0.75); 20 | -webkit-box-shadow: 0px 0px 9px 0px rgba(242, 255, 63, 0.75); 21 | } 22 | /*#turn-timer { 23 | font-size: 13px; 24 | color: #999; 25 | padding: 10px 0; 26 | text-align: center; 27 | }*/ 28 | div#display-outer { 29 | width: 100%; 30 | display: table; 31 | vertical-align: middle; 32 | text-align: center; 33 | margin-top: 38px; 34 | } 35 | div#display { 36 | display: inline-block; 37 | } 38 | .censor { 39 | filter: blur(15px); 40 | -webkit-filter: blur(15px); 41 | } 42 | .censor-fallback { 43 | background-color: white; 44 | } 45 | #warning { 46 | z-index: 1; 47 | position: fixed; 48 | margin: 0; 49 | width: 100%; 50 | height: 100%; 51 | text-align: center; 52 | /*background-color: rgba(0,0,0,.25);*/ 53 | } 54 | table { 55 | color: gray; 56 | } 57 | .user.list-group-item { 58 | color: black; 59 | } 60 | .current-user.list-group-item { 61 | font-style: italic; 62 | } 63 | .admin.list-group-item { 64 | font-weight: bold !important; 65 | color: red !important; 66 | } 67 | .dev.list-group-item { 68 | font-weight: bold !important; 69 | color: blue !important; 70 | } 71 | .waiting-turn.list-group-item { 72 | background-color: #FFFFB3; 73 | } 74 | .has-turn.list-group-item { 75 | background-color: #CFF2FC; 76 | } 77 | .message-pane-wrapper { 78 | border-radius: 4px; 79 | clear: both; 80 | overflow: auto; 81 | position: absolute; 82 | top: 0; 83 | right: 0; 84 | bottom: 0; 85 | left: 0; 86 | height: auto; 87 | width: auto; 88 | /*margin: 0 200px 31px 0;*/ 89 | margin-bottom: 44px; 90 | background-color: #eee; 91 | font-size: 13px; 92 | padding: 0 5px; 93 | } 94 | 95 | .message-pane { 96 | padding-top: 1px; 97 | } 98 | 99 | .message-pane li { 100 | cursor: default; 101 | border-bottom: 1px solid #ccc; 102 | box-shadow: 0 1px 0 0 white; 103 | } 104 | 105 | .message-pane small { 106 | display: none; 107 | font-size: 10px; 108 | position: absolute; 109 | background-color: #f7f7f7; 110 | text-align: center; 111 | line-height: 20px; 112 | margin: 4px 0; 113 | padding: 0 5px; 114 | right: 5px; 115 | } 116 | 117 | .message-pane li:hover { 118 | background-color: #f7f7f7; 119 | } 120 | 121 | .message-pane li:hover small { 122 | display: block; 123 | } 124 | 125 | .message-pane li>div { 126 | overflow: auto; 127 | padding: 2px 0 2px 0; 128 | line-height: 24px; 129 | white-space: -o-pre-wrap; /* Opera */ 130 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 131 | display: inline-block; 132 | } 133 | 134 | .message-pane li>div p { 135 | margin: 0; 136 | } 137 | 138 | .message-pane .username { 139 | font-weight: bold; 140 | white-space: nowrap; 141 | display: block; 142 | float: left; 143 | overflow: hidden; 144 | text-align: right; 145 | color: black; 146 | } 147 | 148 | .message-pane .spacer { 149 | font-size: 14px; 150 | margin-right: 5px; 151 | } 152 | /* Guacamole Keyboard */ 153 | .osk { 154 | width: 100%; 155 | position: relative; 156 | } 157 | 158 | .guac-keyboard { 159 | display: inline-block; 160 | width: 100%; 161 | 162 | margin: 0; 163 | padding: 0; 164 | cursor: default; 165 | 166 | text-align: left; 167 | vertical-align: middle; 168 | } 169 | 170 | .guac-keyboard, 171 | .guac-keyboard * { 172 | overflow: hidden; 173 | white-space: nowrap; 174 | } 175 | 176 | .guac-keyboard .guac-keyboard-key-container { 177 | display: inline-block; 178 | margin: 0.05em; 179 | position: relative; 180 | } 181 | 182 | .guac-keyboard .guac-keyboard-key { 183 | 184 | position: absolute; 185 | left: 0; 186 | right: 0; 187 | top: 0; 188 | bottom: 0; 189 | 190 | background: #444; 191 | 192 | border: 0.125em solid #666; 193 | -moz-border-radius: 0.25em; 194 | -webkit-border-radius: 0.25em; 195 | -khtml-border-radius: 0.25em; 196 | border-radius: 0.25em; 197 | 198 | color: white; 199 | font-size: 40%; 200 | font-weight: lighter; 201 | text-align: center; 202 | white-space: pre; 203 | 204 | text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25), 205 | 1px -1px 0 rgba(0, 0, 0, 0.25), 206 | -1px 1px 0 rgba(0, 0, 0, 0.25), 207 | -1px -1px 0 rgba(0, 0, 0, 0.25); 208 | 209 | } 210 | 211 | .guac-keyboard .guac-keyboard-key:hover { 212 | cursor: pointer; 213 | } 214 | 215 | .guac-keyboard .guac-keyboard-key.highlight { 216 | background: #666; 217 | border-color: #666; 218 | } 219 | 220 | /* Align some keys to the left */ 221 | .guac-keyboard .guac-keyboard-key-caps, 222 | .guac-keyboard .guac-keyboard-key-enter, 223 | .guac-keyboard .guac-keyboard-key-tab, 224 | .guac-keyboard .guac-keyboard-key-lalt, 225 | .guac-keyboard .guac-keyboard-key-ralt, 226 | .guac-keyboard .guac-keyboard-key-lctrl, 227 | .guac-keyboard .guac-keyboard-key-rctrl, 228 | .guac-keyboard .guac-keyboard-key-lshift, 229 | .guac-keyboard .guac-keyboard-key-rshift { 230 | text-align: left; 231 | padding-left: 0.75em; 232 | } 233 | 234 | /* Active shift */ 235 | .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-rshift, 236 | .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-lshift, 237 | 238 | /* Active ctrl */ 239 | .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-rctrl, 240 | .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-lctrl, 241 | 242 | /* Active alt */ 243 | .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-ralt, 244 | .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-lalt, 245 | 246 | /* Active caps */ 247 | .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key-caps, 248 | 249 | /* Active super */ 250 | .guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-super, 251 | .guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-menu { 252 | background: #882; 253 | border-color: #DD4; 254 | } 255 | 256 | .guac-keyboard .guac-keyboard-key.guac-keyboard-disabled { 257 | background: #CCC; 258 | border-color: #BBB; 259 | cursor: default; 260 | } 261 | 262 | .guac-keyboard .guac-keyboard-key.guac-keyboard-pressed { 263 | background: #822; 264 | border-color: #D44; 265 | } 266 | 267 | .guac-keyboard .guac-keyboard-group { 268 | line-height: 0; 269 | } 270 | 271 | .guac-keyboard .guac-keyboard-group.guac-keyboard-alpha, 272 | .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { 273 | display: inline-block; 274 | text-align: center; 275 | vertical-align: top; 276 | } 277 | 278 | .guac-keyboard .guac-keyboard-group.guac-keyboard-main { 279 | 280 | /* IE10 */ 281 | display: -ms-flexbox; 282 | -ms-flex-align: stretch; 283 | -ms-flex-direction: row; 284 | 285 | /* Ancient Mozilla */ 286 | display: -moz-box; 287 | -moz-box-align: stretch; 288 | -moz-box-orient: horizontal; 289 | 290 | /* Ancient WebKit */ 291 | display: -webkit-box; 292 | -webkit-box-align: stretch; 293 | -webkit-box-orient: horizontal; 294 | 295 | /* Old WebKit */ 296 | display: -webkit-flex; 297 | -webkit-align-items: stretch; 298 | -webkit-flex-direction: row; 299 | 300 | /* W3C */ 301 | display: flex; 302 | align-items: stretch; 303 | flex-direction: row; 304 | 305 | } 306 | 307 | .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { 308 | -ms-flex: 1 1 auto; 309 | -moz-box-flex: 1; 310 | -webkit-box-flex: 1; 311 | -webkit-flex: 1 1 auto; 312 | flex: 1 1 auto; 313 | } 314 | 315 | .guac-keyboard .guac-keyboard-gap { 316 | display: inline-block; 317 | } 318 | 319 | /* Hide keycaps requiring modifiers which are NOT currently active. */ 320 | .guac-keyboard:not(.guac-keyboard-modifier-caps) 321 | .guac-keyboard-cap.guac-keyboard-requires-caps, 322 | 323 | .guac-keyboard:not(.guac-keyboard-modifier-shift) 324 | .guac-keyboard-cap.guac-keyboard-requires-shift, 325 | 326 | /* Hide keycaps NOT requiring modifiers which ARE currently active, where that 327 | modifier is used to determine which cap is displayed for the current key. */ 328 | .guac-keyboard.guac-keyboard-modifier-shift 329 | .guac-keyboard-key.guac-keyboard-uses-shift 330 | .guac-keyboard-cap:not(.guac-keyboard-requires-shift), 331 | 332 | .guac-keyboard.guac-keyboard-modifier-caps 333 | .guac-keyboard-key.guac-keyboard-uses-caps 334 | .guac-keyboard-cap:not(.guac-keyboard-requires-caps) { 335 | 336 | display: none; 337 | 338 | } 339 | 340 | /* Spinner */ 341 | .sk-fading-circle { 342 | width: 100%; 343 | height: 100%; 344 | position: relative; 345 | } 346 | 347 | .sk-fading-circle .sk-circle { 348 | width: 100%; 349 | height: 100%; 350 | position: absolute; 351 | left: 0; 352 | top: 0; 353 | } 354 | 355 | .sk-fading-circle .sk-circle:before { 356 | content: ''; 357 | display: block; 358 | margin: 0 auto; 359 | width: 15%; 360 | height: 15%; 361 | background-color: #333; 362 | border-radius: 100%; 363 | -webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both; 364 | animation: sk-circleFadeDelay 1.2s infinite ease-in-out both; 365 | } 366 | .sk-fading-circle .sk-circle2 { 367 | -webkit-transform: rotate(30deg); 368 | -ms-transform: rotate(30deg); 369 | transform: rotate(30deg); 370 | } 371 | .sk-fading-circle .sk-circle3 { 372 | -webkit-transform: rotate(60deg); 373 | -ms-transform: rotate(60deg); 374 | transform: rotate(60deg); 375 | } 376 | .sk-fading-circle .sk-circle4 { 377 | -webkit-transform: rotate(90deg); 378 | -ms-transform: rotate(90deg); 379 | transform: rotate(90deg); 380 | } 381 | .sk-fading-circle .sk-circle5 { 382 | -webkit-transform: rotate(120deg); 383 | -ms-transform: rotate(120deg); 384 | transform: rotate(120deg); 385 | } 386 | .sk-fading-circle .sk-circle6 { 387 | -webkit-transform: rotate(150deg); 388 | -ms-transform: rotate(150deg); 389 | transform: rotate(150deg); 390 | } 391 | .sk-fading-circle .sk-circle7 { 392 | -webkit-transform: rotate(180deg); 393 | -ms-transform: rotate(180deg); 394 | transform: rotate(180deg); 395 | } 396 | .sk-fading-circle .sk-circle8 { 397 | -webkit-transform: rotate(210deg); 398 | -ms-transform: rotate(210deg); 399 | transform: rotate(210deg); 400 | } 401 | .sk-fading-circle .sk-circle9 { 402 | -webkit-transform: rotate(240deg); 403 | -ms-transform: rotate(240deg); 404 | transform: rotate(240deg); 405 | } 406 | .sk-fading-circle .sk-circle10 { 407 | -webkit-transform: rotate(270deg); 408 | -ms-transform: rotate(270deg); 409 | transform: rotate(270deg); 410 | } 411 | .sk-fading-circle .sk-circle11 { 412 | -webkit-transform: rotate(300deg); 413 | -ms-transform: rotate(300deg); 414 | transform: rotate(300deg); 415 | } 416 | .sk-fading-circle .sk-circle12 { 417 | -webkit-transform: rotate(330deg); 418 | -ms-transform: rotate(330deg); 419 | transform: rotate(330deg); 420 | } 421 | .sk-fading-circle .sk-circle2:before { 422 | -webkit-animation-delay: -1.1s; 423 | animation-delay: -1.1s; 424 | } 425 | .sk-fading-circle .sk-circle3:before { 426 | -webkit-animation-delay: -1s; 427 | animation-delay: -1s; 428 | } 429 | .sk-fading-circle .sk-circle4:before { 430 | -webkit-animation-delay: -0.9s; 431 | animation-delay: -0.9s; 432 | } 433 | .sk-fading-circle .sk-circle5:before { 434 | -webkit-animation-delay: -0.8s; 435 | animation-delay: -0.8s; 436 | } 437 | .sk-fading-circle .sk-circle6:before { 438 | -webkit-animation-delay: -0.7s; 439 | animation-delay: -0.7s; 440 | } 441 | .sk-fading-circle .sk-circle7:before { 442 | -webkit-animation-delay: -0.6s; 443 | animation-delay: -0.6s; 444 | } 445 | .sk-fading-circle .sk-circle8:before { 446 | -webkit-animation-delay: -0.5s; 447 | animation-delay: -0.5s; 448 | } 449 | .sk-fading-circle .sk-circle9:before { 450 | -webkit-animation-delay: -0.4s; 451 | animation-delay: -0.4s; 452 | } 453 | .sk-fading-circle .sk-circle10:before { 454 | -webkit-animation-delay: -0.3s; 455 | animation-delay: -0.3s; 456 | } 457 | .sk-fading-circle .sk-circle11:before { 458 | -webkit-animation-delay: -0.2s; 459 | animation-delay: -0.2s; 460 | } 461 | .sk-fading-circle .sk-circle12:before { 462 | -webkit-animation-delay: -0.1s; 463 | animation-delay: -0.1s; 464 | } 465 | 466 | @-webkit-keyframes sk-circleFadeDelay { 467 | 0%, 39%, 100% { opacity: 0; } 468 | 40% { opacity: 1; } 469 | } 470 | 471 | @keyframes sk-circleFadeDelay { 472 | 0%, 39%, 100% { opacity: 0; } 473 | 40% { opacity: 1; } 474 | } 475 | -------------------------------------------------------------------------------- /src/html/news.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | collab-vm Donations 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 37 | 38 |
39 |

Latest News

40 |
Get the latest news about Collab VM! 41 |

1/14/2016 - "OMG THEMES?"

42 |
Good evening everyone. You may have noticed on the Navbar there is a new option called "Themes". Yes, it's true. Starting
today, collab-vm officially supports a multitude of themes. This includes the classic theme, a dark theme, a XP-like theme, and a few others.
We hope you enjoy and in the future we will probably add an option to customize your own. Enjoy it! 43 |

-Dartz
44 |

12/21/2015 - "Collab VM 1.1 released!"

45 |
Good evening everybody.

Today, me (Dartz) and Cosmic Sans (a good friend & the main coder) have officially released
Collab VM 1.1. Collab VM 1.1 has several updates to enhance the experience of this site.
Read more at the reddit post here

We really hope you enjoy this update!

-Dartz 46 |
47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /src/html/notify.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/html/notify.m4a -------------------------------------------------------------------------------- /src/html/notify.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/html/notify.mp3 -------------------------------------------------------------------------------- /src/html/notify.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/html/notify.ogg -------------------------------------------------------------------------------- /src/html/rules.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | collab-vm Donations 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 37 | 38 |
39 |

Rules

40 |
Here are the rules for CollabVM. If you break any of them, expect a ban. 41 |

R1. Do not do anything illegal in the United States or Canada.

42 |
A. Do not do any action that is illegal in the United States or Canada (e.g. downloading illegal content). 43 |

R2. No running DDoSing tools.

44 |
A. Do not use CollabVM to DDoS an indivdiual, business, company, or anyone else. 45 |

R3. No *coin miners.

46 |
A. Do not run any *coin miners (bitcoin, litecoin, dogecoin, etc). 47 |

R4. No email spamming.

48 |
A. Do not spam any emails using this service. 49 |

R5. Do not abuse any exploits.

50 |
A. Do not abuse any exploits you find. Instead, report them to me at rightowner@gmail.com. NOTE: RATs, TeamViewer, and other tools do not count as "exploiting" or "cheating". 51 |

R6. Don't impersonate other users.

52 |
A. Do not impersonate other members of CollabVM. If caught you'll be temporarily disconnected. 53 |

R7. One vote per person.

54 |
A. Do not use any tools (including, but not limited to, proxies, VPNs, VNCs, 2G/3G/4G connections, Tor nodes, etc) to bypass the vote once restriction. Only one vote per person is allowed, no matter what. Anybody who is caught doing this will be warned, then banned for two weeks. 55 |

What you will NOT be banned for.

56 |
Destroying Windows.
Being a dick. (pls don't though its not nice.)
Downloading viruses. (Unless they violate any of the rules above).

57 |
REMEMBER: You are responsible for everything you do on this machine! 58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /src/html/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/html/test.png -------------------------------------------------------------------------------- /src/js/collab-vm/common.js: -------------------------------------------------------------------------------- 1 | // CollabVM webapp configuration 2 | 3 | const root="/collab-vm/"; 4 | 5 | export default { 6 | DEBUG: false, 7 | DEBUG_NO_TIMEOUT: false, 8 | DEBUG_NO_NSFW: false, 9 | DEBUG_NO_CONNECT: false, // is this used? 10 | DEBUG_VM_LIST: false, 11 | DEBUG_VM_LIST: false, 12 | 13 | debugLog: function() { 14 | if(this.DEBUG) 15 | console.log.apply(null, arguments); 16 | }, 17 | 18 | /** @const 19 | * The root directory of the collab-vm project with a 20 | * forward slash appended to it. 21 | * This is determined at runtime to allow the project to be 22 | * relocated without needing to recompile the javascript. 23 | * @type {string} 24 | */ 25 | rootDir: root, 26 | chatSound: root + "notify", 27 | 28 | /** 29 | * The main node this webapp is configured to connect to. 30 | * @const 31 | */ 32 | serverAddress: window.location.host, 33 | 34 | /** 35 | * Additional nodes to connect to. 36 | * Uses multicollab() to do so 37 | * @const 38 | */ 39 | additionalNodes: [ 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /src/js/collab-vm/en-us-qwerty.js: -------------------------------------------------------------------------------- 1 | export var en_us_qwerty_keyboard = { 2 | 3 | "language" : "en_US", 4 | "type" : "qwerty", 5 | "width" : 22, 6 | 7 | "keys" : { 8 | 9 | "Back" : 65288, 10 | "Tab" : 65289, 11 | "Enter" : 65293, 12 | "Esc" : 65307, 13 | "Home" : 65360, 14 | "PgUp" : 65365, 15 | "PgDn" : 65366, 16 | "End" : 65367, 17 | "Ins" : 65379, 18 | "F1" : 65470, 19 | "F2" : 65471, 20 | "F3" : 65472, 21 | "F4" : 65473, 22 | "F5" : 65474, 23 | "F6" : 65475, 24 | "F7" : 65476, 25 | "F8" : 65477, 26 | "F9" : 65478, 27 | "F10" : 65479, 28 | "F11" : 65480, 29 | "F12" : 65481, 30 | "Del" : 65535, 31 | 32 | "Space" : " ", 33 | 34 | "Left" : [{ 35 | "title" : "←", 36 | "keysym" : 65361 37 | }], 38 | "Up" : [{ 39 | "title" : "↑", 40 | "keysym" : 65362 41 | }], 42 | "Right" : [{ 43 | "title" : "→", 44 | "keysym" : 65363 45 | }], 46 | "Down" : [{ 47 | "title" : "↓", 48 | "keysym" : 65364 49 | }], 50 | 51 | "Menu" : [{ 52 | "title" : "Menu", 53 | "modifier" : "super", 54 | "keysym" : 65383 55 | }], 56 | "LShift" : [{ 57 | "title" : "Shift", 58 | "modifier" : "shift", 59 | "keysym" : 65505 60 | }], 61 | "RShift" : [{ 62 | "title" : "Shift", 63 | "modifier" : "shift", 64 | "keysym" : 65506 65 | }], 66 | "LCtrl" : [{ 67 | "title" : "Ctrl", 68 | "modifier" : "control", 69 | "keysym" : 65507 70 | }], 71 | "RCtrl" : [{ 72 | "title" : "Ctrl", 73 | "modifier" : "control", 74 | "keysym" : 65508 75 | }], 76 | "Caps" : [{ 77 | "title" : "Caps", 78 | "modifier" : "caps", 79 | "keysym" : 65509 80 | }], 81 | "LAlt" : [{ 82 | "title" : "Alt", 83 | "modifier" : "alt", 84 | "keysym" : 65513 85 | }], 86 | "RAlt" : [{ 87 | "title" : "Alt", 88 | "modifier" : "alt", 89 | "keysym" : 65514 90 | }], 91 | "Super" : [{ 92 | "title" : "Super", 93 | "modifier" : "super", 94 | "keysym" : 65515 95 | }], 96 | 97 | "`" : [ 98 | { "title" : "`", "requires" : [ ] }, 99 | { "title" : "~", "requires" : [ "shift" ] } 100 | ], 101 | "1" : [ 102 | { "title" : "1", "requires" : [ ] }, 103 | { "title" : "!", "requires" : [ "shift" ] } 104 | ], 105 | "2" : [ 106 | { "title" : "2", "requires" : [ ] }, 107 | { "title" : "@", "requires" : [ "shift" ] } 108 | ], 109 | "3" : [ 110 | { "title" : "3", "requires" : [ ] }, 111 | { "title" : "#", "requires" : [ "shift" ] } 112 | ], 113 | "4" : [ 114 | { "title" : "4", "requires" : [ ] }, 115 | { "title" : "$", "requires" : [ "shift" ] } 116 | ], 117 | "5" : [ 118 | { "title" : "5", "requires" : [ ] }, 119 | { "title" : "%", "requires" : [ "shift" ] } 120 | ], 121 | "6" : [ 122 | { "title" : "6", "requires" : [ ] }, 123 | { "title" : "^", "requires" : [ "shift" ] } 124 | ], 125 | "7" : [ 126 | { "title" : "7", "requires" : [ ] }, 127 | { "title" : "&", "requires" : [ "shift" ] } 128 | ], 129 | "8" : [ 130 | { "title" : "8", "requires" : [ ] }, 131 | { "title" : "*", "requires" : [ "shift" ] } 132 | ], 133 | "9" : [ 134 | { "title" : "9", "requires" : [ ] }, 135 | { "title" : "(", "requires" : [ "shift" ] } 136 | ], 137 | "0" : [ 138 | { "title" : "0", "requires" : [ ] }, 139 | { "title" : ")", "requires" : [ "shift" ] } 140 | ], 141 | "-" : [ 142 | { "title" : "-", "requires" : [ ] }, 143 | { "title" : "_", "requires" : [ "shift" ] } 144 | ], 145 | "=" : [ 146 | { "title" : "=", "requires" : [ ] }, 147 | { "title" : "+", "requires" : [ "shift" ] } 148 | ], 149 | "," : [ 150 | { "title" : ",", "requires" : [ ] }, 151 | { "title" : "<", "requires" : [ "shift" ] } 152 | ], 153 | "." : [ 154 | { "title" : ".", "requires" : [ ] }, 155 | { "title" : ">", "requires" : [ "shift" ] } 156 | ], 157 | "/" : [ 158 | { "title" : "/", "requires" : [ ] }, 159 | { "title" : "?", "requires" : [ "shift" ] } 160 | ], 161 | "[" : [ 162 | { "title" : "[", "requires" : [ ] }, 163 | { "title" : "{", "requires" : [ "shift" ] } 164 | ], 165 | "]" : [ 166 | { "title" : "]", "requires" : [ ] }, 167 | { "title" : "}", "requires" : [ "shift" ] } 168 | ], 169 | "\\" : [ 170 | { "title" : "\\", "requires" : [ ] }, 171 | { "title" : "|", "requires" : [ "shift" ] } 172 | ], 173 | ";" : [ 174 | { "title" : ";", "requires" : [ ] }, 175 | { "title" : ":", "requires" : [ "shift" ] } 176 | ], 177 | "'" : [ 178 | { "title" : "'", "requires" : [ ] }, 179 | { "title" : "\"", "requires" : [ "shift" ] } 180 | ], 181 | 182 | "q" : [ 183 | { "title" : "q", "requires" : [ ] }, 184 | { "title" : "Q", "requires" : [ "caps" ] }, 185 | { "title" : "Q", "requires" : [ "shift" ] }, 186 | { "title" : "q", "requires" : [ "caps", "shift" ] } 187 | ], 188 | "w" : [ 189 | { "title" : "w", "requires" : [ ] }, 190 | { "title" : "W", "requires" : [ "caps" ] }, 191 | { "title" : "W", "requires" : [ "shift" ] }, 192 | { "title" : "w", "requires" : [ "caps", "shift" ] } 193 | ], 194 | "e" : [ 195 | { "title" : "e", "requires" : [ ] }, 196 | { "title" : "E", "requires" : [ "caps" ] }, 197 | { "title" : "E", "requires" : [ "shift" ] }, 198 | { "title" : "e", "requires" : [ "caps", "shift" ] } 199 | ], 200 | "r" : [ 201 | { "title" : "r", "requires" : [ ] }, 202 | { "title" : "R", "requires" : [ "caps" ] }, 203 | { "title" : "R", "requires" : [ "shift" ] }, 204 | { "title" : "r", "requires" : [ "caps", "shift" ] } 205 | ], 206 | "t" : [ 207 | { "title" : "t", "requires" : [ ] }, 208 | { "title" : "T", "requires" : [ "caps" ] }, 209 | { "title" : "T", "requires" : [ "shift" ] }, 210 | { "title" : "t", "requires" : [ "caps", "shift" ] } 211 | ], 212 | "y" : [ 213 | { "title" : "y", "requires" : [ ] }, 214 | { "title" : "Y", "requires" : [ "caps" ] }, 215 | { "title" : "Y", "requires" : [ "shift" ] }, 216 | { "title" : "y", "requires" : [ "caps", "shift" ] } 217 | ], 218 | "u" : [ 219 | { "title" : "u", "requires" : [ ] }, 220 | { "title" : "U", "requires" : [ "caps" ] }, 221 | { "title" : "U", "requires" : [ "shift" ] }, 222 | { "title" : "u", "requires" : [ "caps", "shift" ] } 223 | ], 224 | "i" : [ 225 | { "title" : "i", "requires" : [ ] }, 226 | { "title" : "I", "requires" : [ "caps" ] }, 227 | { "title" : "I", "requires" : [ "shift" ] }, 228 | { "title" : "i", "requires" : [ "caps", "shift" ] } 229 | ], 230 | "o" : [ 231 | { "title" : "o", "requires" : [ ] }, 232 | { "title" : "O", "requires" : [ "caps" ] }, 233 | { "title" : "O", "requires" : [ "shift" ] }, 234 | { "title" : "o", "requires" : [ "caps", "shift" ] } 235 | ], 236 | "p" : [ 237 | { "title" : "p", "requires" : [ ] }, 238 | { "title" : "P", "requires" : [ "caps" ] }, 239 | { "title" : "P", "requires" : [ "shift" ] }, 240 | { "title" : "p", "requires" : [ "caps", "shift" ] } 241 | ], 242 | "a" : [ 243 | { "title" : "a", "requires" : [ ] }, 244 | { "title" : "A", "requires" : [ "caps" ] }, 245 | { "title" : "A", "requires" : [ "shift" ] }, 246 | { "title" : "a", "requires" : [ "caps", "shift" ] } 247 | ], 248 | "s" : [ 249 | { "title" : "s", "requires" : [ ] }, 250 | { "title" : "S", "requires" : [ "caps" ] }, 251 | { "title" : "S", "requires" : [ "shift" ] }, 252 | { "title" : "s", "requires" : [ "caps", "shift" ] } 253 | ], 254 | "d" : [ 255 | { "title" : "d", "requires" : [ ] }, 256 | { "title" : "D", "requires" : [ "caps" ] }, 257 | { "title" : "D", "requires" : [ "shift" ] }, 258 | { "title" : "d", "requires" : [ "caps", "shift" ] } 259 | ], 260 | "f" : [ 261 | { "title" : "f", "requires" : [ ] }, 262 | { "title" : "F", "requires" : [ "caps" ] }, 263 | { "title" : "F", "requires" : [ "shift" ] }, 264 | { "title" : "f", "requires" : [ "caps", "shift" ] } 265 | ], 266 | "g" : [ 267 | { "title" : "g", "requires" : [ ] }, 268 | { "title" : "G", "requires" : [ "caps" ] }, 269 | { "title" : "G", "requires" : [ "shift" ] }, 270 | { "title" : "g", "requires" : [ "caps", "shift" ] } 271 | ], 272 | "h" : [ 273 | { "title" : "h", "requires" : [ ] }, 274 | { "title" : "H", "requires" : [ "caps" ] }, 275 | { "title" : "H", "requires" : [ "shift" ] }, 276 | { "title" : "h", "requires" : [ "caps", "shift" ] } 277 | ], 278 | "j" : [ 279 | { "title" : "j", "requires" : [ ] }, 280 | { "title" : "J", "requires" : [ "caps" ] }, 281 | { "title" : "J", "requires" : [ "shift" ] }, 282 | { "title" : "j", "requires" : [ "caps", "shift" ] } 283 | ], 284 | "k" : [ 285 | { "title" : "k", "requires" : [ ] }, 286 | { "title" : "K", "requires" : [ "caps" ] }, 287 | { "title" : "K", "requires" : [ "shift" ] }, 288 | { "title" : "k", "requires" : [ "caps", "shift" ] } 289 | ], 290 | "l" : [ 291 | { "title" : "l", "requires" : [ ] }, 292 | { "title" : "L", "requires" : [ "caps" ] }, 293 | { "title" : "L", "requires" : [ "shift" ] }, 294 | { "title" : "l", "requires" : [ "caps", "shift" ] } 295 | ], 296 | "z" : [ 297 | { "title" : "z", "requires" : [ ] }, 298 | { "title" : "Z", "requires" : [ "caps" ] }, 299 | { "title" : "Z", "requires" : [ "shift" ] }, 300 | { "title" : "z", "requires" : [ "caps", "shift" ] } 301 | ], 302 | "x" : [ 303 | { "title" : "x", "requires" : [ ] }, 304 | { "title" : "X", "requires" : [ "caps" ] }, 305 | { "title" : "X", "requires" : [ "shift" ] }, 306 | { "title" : "x", "requires" : [ "caps", "shift" ] } 307 | ], 308 | "c" : [ 309 | { "title" : "c", "requires" : [ ] }, 310 | { "title" : "C", "requires" : [ "caps" ] }, 311 | { "title" : "C", "requires" : [ "shift" ] }, 312 | { "title" : "c", "requires" : [ "caps", "shift" ] } 313 | ], 314 | "v" : [ 315 | { "title" : "v", "requires" : [ ] }, 316 | { "title" : "V", "requires" : [ "caps" ] }, 317 | { "title" : "V", "requires" : [ "shift" ] }, 318 | { "title" : "v", "requires" : [ "caps", "shift" ] } 319 | ], 320 | "b" : [ 321 | { "title" : "b", "requires" : [ ] }, 322 | { "title" : "B", "requires" : [ "caps" ] }, 323 | { "title" : "B", "requires" : [ "shift" ] }, 324 | { "title" : "b", "requires" : [ "caps", "shift" ] } 325 | ], 326 | "n" : [ 327 | { "title" : "n", "requires" : [ ] }, 328 | { "title" : "N", "requires" : [ "caps" ] }, 329 | { "title" : "N", "requires" : [ "shift" ] }, 330 | { "title" : "n", "requires" : [ "caps", "shift" ] } 331 | ], 332 | "m" : [ 333 | { "title" : "m", "requires" : [ ] }, 334 | { "title" : "M", "requires" : [ "caps" ] }, 335 | { "title" : "M", "requires" : [ "shift" ] }, 336 | { "title" : "m", "requires" : [ "caps", "shift" ] } 337 | ] 338 | 339 | }, 340 | 341 | "layout" : [ 342 | 343 | [ "Esc", 0.7, "F1", "F2", "F3", "F4", 344 | 0.7, "F5", "F6", "F7", "F8", 345 | 0.7, "F9", "F10", "F11", "F12" ], 346 | 347 | [ 0.1 ], 348 | 349 | { 350 | "main" : { 351 | "alpha" : [ 352 | 353 | [ "`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Back" ], 354 | [ "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\" ], 355 | [ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "Enter" ], 356 | [ "LShift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "RShift" ], 357 | [ "LCtrl", "Super", "LAlt", "Space", "RAlt", "Menu", "RCtrl" ] 358 | 359 | ], 360 | 361 | "movement" : [ 362 | [ "Ins", "Home", "PgUp" ], 363 | [ "Del", "End", "PgDn" ], 364 | [ 1 ], 365 | [ "Up" ], 366 | [ "Left", "Down", "Right" ] 367 | ] 368 | } 369 | } 370 | 371 | ], 372 | 373 | "keyWidths" : { 374 | 375 | "Back" : 2, 376 | "Tab" : 1.5, 377 | "\\" : 1.5, 378 | "Caps" : 1.85, 379 | "Enter" : 2.25, 380 | "LShift" : 2.1, 381 | "RShift" : 3.1, 382 | 383 | "LCtrl" : 1.6, 384 | "Super" : 1.6, 385 | "LAlt" : 1.6, 386 | "Space" : 6.1, 387 | "RAlt" : 1.6, 388 | "Menu" : 1.6, 389 | "RCtrl" : 1.6, 390 | 391 | "Ins" : 1.6, 392 | "Home" : 1.6, 393 | "PgUp" : 1.6, 394 | "Del" : 1.6, 395 | "End" : 1.6, 396 | "PgDn" : 1.6 397 | 398 | } 399 | 400 | }; 401 | -------------------------------------------------------------------------------- /src/js/guacamole/ArrayBufferReader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Glyptodon LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | var Guacamole = Guacamole || {}; 24 | 25 | /** 26 | * A reader which automatically handles the given input stream, returning 27 | * strictly received packets as array buffers. Note that this object will 28 | * overwrite any installed event handlers on the given Guacamole.InputStream. 29 | * 30 | * @constructor 31 | * @param {Guacamole.InputStream} stream The stream that data will be read 32 | * from. 33 | */ 34 | Guacamole.ArrayBufferReader = function(stream) { 35 | 36 | /** 37 | * Reference to this Guacamole.InputStream. 38 | * @private 39 | */ 40 | var guac_reader = this; 41 | 42 | // Receive blobs as array buffers 43 | stream.onblob = function(data) { 44 | 45 | // Convert to ArrayBuffer 46 | var binary = window.atob(data); 47 | var arrayBuffer = new ArrayBuffer(binary.length); 48 | var bufferView = new Uint8Array(arrayBuffer); 49 | 50 | for (var i=0; i 0) 59 | return pool.shift(); 60 | 61 | // Otherwise, return a new integer 62 | return guac_pool.next_int++; 63 | 64 | }; 65 | 66 | /** 67 | * Frees the given integer, allowing it to be reused. 68 | * 69 | * @param {Number} integer The integer to free. 70 | */ 71 | this.free = function(integer) { 72 | pool.push(integer); 73 | }; 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /src/js/guacamole/Layer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Glyptodon LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | var Guacamole = Guacamole || {}; 24 | 25 | /** 26 | * Abstract ordered drawing surface. Each Layer contains a canvas element and 27 | * provides simple drawing instructions for drawing to that canvas element, 28 | * however unlike the canvas element itself, drawing operations on a Layer are 29 | * guaranteed to run in order, even if such an operation must wait for an image 30 | * to load before completing. 31 | * 32 | * @constructor 33 | * 34 | * @param {Number} width The width of the Layer, in pixels. The canvas element 35 | * backing this Layer will be given this width. 36 | * 37 | * @param {Number} height The height of the Layer, in pixels. The canvas element 38 | * backing this Layer will be given this height. 39 | */ 40 | Guacamole.Layer = function(width, height) { 41 | 42 | /** 43 | * Reference to this Layer. 44 | * @private 45 | */ 46 | var layer = this; 47 | 48 | /** 49 | * The canvas element backing this Layer. 50 | * @private 51 | */ 52 | var canvas = document.createElement("canvas"); 53 | 54 | /** 55 | * The 2D display context of the canvas element backing this Layer. 56 | * @private 57 | */ 58 | var context = canvas.getContext("2d"); 59 | context.save(); 60 | 61 | /** 62 | * Whether a new path should be started with the next path drawing 63 | * operations. 64 | * @private 65 | */ 66 | var pathClosed = true; 67 | 68 | /** 69 | * The number of states on the state stack. 70 | * 71 | * Note that there will ALWAYS be one element on the stack, but that 72 | * element is not exposed. It is only used to reset the layer to its 73 | * initial state. 74 | * 75 | * @private 76 | */ 77 | var stackSize = 0; 78 | 79 | /** 80 | * Map of all Guacamole channel masks to HTML5 canvas composite operation 81 | * names. Not all channel mask combinations are currently implemented. 82 | * @private 83 | */ 84 | var compositeOperation = { 85 | /* 0x0 NOT IMPLEMENTED */ 86 | 0x1: "destination-in", 87 | 0x2: "destination-out", 88 | /* 0x3 NOT IMPLEMENTED */ 89 | 0x4: "source-in", 90 | /* 0x5 NOT IMPLEMENTED */ 91 | 0x6: "source-atop", 92 | /* 0x7 NOT IMPLEMENTED */ 93 | 0x8: "source-out", 94 | 0x9: "destination-atop", 95 | 0xA: "xor", 96 | 0xB: "destination-over", 97 | 0xC: "copy", 98 | /* 0xD NOT IMPLEMENTED */ 99 | 0xE: "source-over", 100 | 0xF: "lighter" 101 | }; 102 | 103 | /** 104 | * Resizes the canvas element backing this Layer without testing the 105 | * new size. This function should only be used internally. 106 | * 107 | * @private 108 | * @param {Number} newWidth The new width to assign to this Layer. 109 | * @param {Number} newHeight The new height to assign to this Layer. 110 | */ 111 | function resize(newWidth, newHeight) { 112 | 113 | // Only preserve old data if width/height are both non-zero 114 | var oldData = null; 115 | if (layer.width !== 0 && layer.height !== 0) { 116 | 117 | // Create canvas and context for holding old data 118 | oldData = document.createElement("canvas"); 119 | oldData.width = layer.width; 120 | oldData.height = layer.height; 121 | 122 | var oldDataContext = oldData.getContext("2d"); 123 | 124 | // Copy image data from current 125 | oldDataContext.drawImage(canvas, 126 | 0, 0, layer.width, layer.height, 127 | 0, 0, layer.width, layer.height); 128 | 129 | } 130 | 131 | // Preserve composite operation 132 | var oldCompositeOperation = context.globalCompositeOperation; 133 | 134 | // Resize canvas 135 | canvas.width = newWidth; 136 | canvas.height = newHeight; 137 | 138 | // Redraw old data, if any 139 | if (oldData) 140 | context.drawImage(oldData, 141 | 0, 0, layer.width, layer.height, 142 | 0, 0, layer.width, layer.height); 143 | 144 | // Restore composite operation 145 | context.globalCompositeOperation = oldCompositeOperation; 146 | 147 | layer.width = newWidth; 148 | layer.height = newHeight; 149 | 150 | // Acknowledge reset of stack (happens on resize of canvas) 151 | stackSize = 0; 152 | context.save(); 153 | 154 | } 155 | 156 | /** 157 | * Given the X and Y coordinates of the upper-left corner of a rectangle 158 | * and the rectangle's width and height, resize the backing canvas element 159 | * as necessary to ensure that the rectangle fits within the canvas 160 | * element's coordinate space. This function will only make the canvas 161 | * larger. If the rectangle already fits within the canvas element's 162 | * coordinate space, the canvas is left unchanged. 163 | * 164 | * @private 165 | * @param {Number} x The X coordinate of the upper-left corner of the 166 | * rectangle to fit. 167 | * @param {Number} y The Y coordinate of the upper-left corner of the 168 | * rectangle to fit. 169 | * @param {Number} w The width of the the rectangle to fit. 170 | * @param {Number} h The height of the the rectangle to fit. 171 | */ 172 | function fitRect(x, y, w, h) { 173 | 174 | // Calculate bounds 175 | var opBoundX = w + x; 176 | var opBoundY = h + y; 177 | 178 | // Determine max width 179 | var resizeWidth; 180 | if (opBoundX > layer.width) 181 | resizeWidth = opBoundX; 182 | else 183 | resizeWidth = layer.width; 184 | 185 | // Determine max height 186 | var resizeHeight; 187 | if (opBoundY > layer.height) 188 | resizeHeight = opBoundY; 189 | else 190 | resizeHeight = layer.height; 191 | 192 | // Resize if necessary 193 | layer.resize(resizeWidth, resizeHeight); 194 | 195 | } 196 | 197 | /** 198 | * Set to true if this Layer should resize itself to accomodate the 199 | * dimensions of any drawing operation, and false (the default) otherwise. 200 | * 201 | * Note that setting this property takes effect immediately, and thus may 202 | * take effect on operations that were started in the past but have not 203 | * yet completed. If you wish the setting of this flag to only modify 204 | * future operations, you will need to make the setting of this flag an 205 | * operation with sync(). 206 | * 207 | * @example 208 | * // Set autosize to true for all future operations 209 | * layer.sync(function() { 210 | * layer.autosize = true; 211 | * }); 212 | * 213 | * @type Boolean 214 | * @default false 215 | */ 216 | this.autosize = false; 217 | 218 | /** 219 | * The current width of this layer. 220 | * @type Number 221 | */ 222 | this.width = width; 223 | 224 | /** 225 | * The current height of this layer. 226 | * @type Number 227 | */ 228 | this.height = height; 229 | 230 | /** 231 | * Returns the canvas element backing this Layer. 232 | * @returns {Element} The canvas element backing this Layer. 233 | */ 234 | this.getCanvas = function() { 235 | return canvas; 236 | }; 237 | 238 | /** 239 | * Changes the size of this Layer to the given width and height. Resizing 240 | * is only attempted if the new size provided is actually different from 241 | * the current size. 242 | * 243 | * @param {Number} newWidth The new width to assign to this Layer. 244 | * @param {Number} newHeight The new height to assign to this Layer. 245 | */ 246 | this.resize = function(newWidth, newHeight) { 247 | if (newWidth !== layer.width || newHeight !== layer.height) 248 | resize(newWidth, newHeight); 249 | }; 250 | 251 | /** 252 | * Draws the specified image at the given coordinates. The image specified 253 | * must already be loaded. 254 | * 255 | * @param {Number} x The destination X coordinate. 256 | * @param {Number} y The destination Y coordinate. 257 | * @param {Image} image The image to draw. Note that this is an Image 258 | * object - not a URL. 259 | */ 260 | this.drawImage = function(x, y, image) { 261 | if (layer.autosize) fitRect(x, y, image.width, image.height); 262 | context.drawImage(image, x, y); 263 | }; 264 | 265 | /** 266 | * Transfer a rectangle of image data from one Layer to this Layer using the 267 | * specified transfer function. 268 | * 269 | * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 270 | * @param {Number} srcx The X coordinate of the upper-left corner of the 271 | * rectangle within the source Layer's coordinate 272 | * space to copy data from. 273 | * @param {Number} srcy The Y coordinate of the upper-left corner of the 274 | * rectangle within the source Layer's coordinate 275 | * space to copy data from. 276 | * @param {Number} srcw The width of the rectangle within the source Layer's 277 | * coordinate space to copy data from. 278 | * @param {Number} srch The height of the rectangle within the source 279 | * Layer's coordinate space to copy data from. 280 | * @param {Number} x The destination X coordinate. 281 | * @param {Number} y The destination Y coordinate. 282 | * @param {Function} transferFunction The transfer function to use to 283 | * transfer data from source to 284 | * destination. 285 | */ 286 | this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) { 287 | 288 | var srcCanvas = srcLayer.getCanvas(); 289 | 290 | // If entire rectangle outside source canvas, stop 291 | if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; 292 | 293 | // Otherwise, clip rectangle to area 294 | if (srcx + srcw > srcCanvas.width) 295 | srcw = srcCanvas.width - srcx; 296 | 297 | if (srcy + srch > srcCanvas.height) 298 | srch = srcCanvas.height - srcy; 299 | 300 | // Stop if nothing to draw. 301 | if (srcw === 0 || srch === 0) return; 302 | 303 | if (layer.autosize) fitRect(x, y, srcw, srch); 304 | 305 | // Get image data from src and dst 306 | var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); 307 | var dst = context.getImageData(x , y, srcw, srch); 308 | 309 | // Apply transfer for each pixel 310 | for (var i=0; i= srcCanvas.width || srcy >= srcCanvas.height) return; 368 | 369 | // Otherwise, clip rectangle to area 370 | if (srcx + srcw > srcCanvas.width) 371 | srcw = srcCanvas.width - srcx; 372 | 373 | if (srcy + srch > srcCanvas.height) 374 | srch = srcCanvas.height - srcy; 375 | 376 | // Stop if nothing to draw. 377 | if (srcw === 0 || srch === 0) return; 378 | 379 | if (layer.autosize) fitRect(x, y, srcw, srch); 380 | 381 | // Get image data from src and dst 382 | var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch); 383 | context.putImageData(src, x, y); 384 | 385 | }; 386 | 387 | /** 388 | * Copy a rectangle of image data from one Layer to this Layer. This 389 | * operation will copy exactly the image data that will be drawn once all 390 | * operations of the source Layer that were pending at the time this 391 | * function was called are complete. This operation will not alter the 392 | * size of the source Layer even if its autosize property is set to true. 393 | * 394 | * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. 395 | * @param {Number} srcx The X coordinate of the upper-left corner of the 396 | * rectangle within the source Layer's coordinate 397 | * space to copy data from. 398 | * @param {Number} srcy The Y coordinate of the upper-left corner of the 399 | * rectangle within the source Layer's coordinate 400 | * space to copy data from. 401 | * @param {Number} srcw The width of the rectangle within the source Layer's 402 | * coordinate space to copy data from. 403 | * @param {Number} srch The height of the rectangle within the source 404 | * Layer's coordinate space to copy data from. 405 | * @param {Number} x The destination X coordinate. 406 | * @param {Number} y The destination Y coordinate. 407 | */ 408 | this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) { 409 | 410 | var srcCanvas = srcLayer.getCanvas(); 411 | 412 | // If entire rectangle outside source canvas, stop 413 | if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return; 414 | 415 | // Otherwise, clip rectangle to area 416 | if (srcx + srcw > srcCanvas.width) 417 | srcw = srcCanvas.width - srcx; 418 | 419 | if (srcy + srch > srcCanvas.height) 420 | srch = srcCanvas.height - srcy; 421 | 422 | // Stop if nothing to draw. 423 | if (srcw === 0 || srch === 0) return; 424 | 425 | if (layer.autosize) fitRect(x, y, srcw, srch); 426 | context.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch); 427 | 428 | }; 429 | 430 | /** 431 | * Starts a new path at the specified point. 432 | * 433 | * @param {Number} x The X coordinate of the point to draw. 434 | * @param {Number} y The Y coordinate of the point to draw. 435 | */ 436 | this.moveTo = function(x, y) { 437 | 438 | // Start a new path if current path is closed 439 | if (pathClosed) { 440 | context.beginPath(); 441 | pathClosed = false; 442 | } 443 | 444 | if (layer.autosize) fitRect(x, y, 0, 0); 445 | context.moveTo(x, y); 446 | 447 | }; 448 | 449 | /** 450 | * Add the specified line to the current path. 451 | * 452 | * @param {Number} x The X coordinate of the endpoint of the line to draw. 453 | * @param {Number} y The Y coordinate of the endpoint of the line to draw. 454 | */ 455 | this.lineTo = function(x, y) { 456 | 457 | // Start a new path if current path is closed 458 | if (pathClosed) { 459 | context.beginPath(); 460 | pathClosed = false; 461 | } 462 | 463 | if (layer.autosize) fitRect(x, y, 0, 0); 464 | context.lineTo(x, y); 465 | 466 | }; 467 | 468 | /** 469 | * Add the specified arc to the current path. 470 | * 471 | * @param {Number} x The X coordinate of the center of the circle which 472 | * will contain the arc. 473 | * @param {Number} y The Y coordinate of the center of the circle which 474 | * will contain the arc. 475 | * @param {Number} radius The radius of the circle. 476 | * @param {Number} startAngle The starting angle of the arc, in radians. 477 | * @param {Number} endAngle The ending angle of the arc, in radians. 478 | * @param {Boolean} negative Whether the arc should be drawn in order of 479 | * decreasing angle. 480 | */ 481 | this.arc = function(x, y, radius, startAngle, endAngle, negative) { 482 | 483 | // Start a new path if current path is closed 484 | if (pathClosed) { 485 | context.beginPath(); 486 | pathClosed = false; 487 | } 488 | 489 | if (layer.autosize) fitRect(x, y, 0, 0); 490 | context.arc(x, y, radius, startAngle, endAngle, negative); 491 | 492 | }; 493 | 494 | /** 495 | * Starts a new path at the specified point. 496 | * 497 | * @param {Number} cp1x The X coordinate of the first control point. 498 | * @param {Number} cp1y The Y coordinate of the first control point. 499 | * @param {Number} cp2x The X coordinate of the second control point. 500 | * @param {Number} cp2y The Y coordinate of the second control point. 501 | * @param {Number} x The X coordinate of the endpoint of the curve. 502 | * @param {Number} y The Y coordinate of the endpoint of the curve. 503 | */ 504 | this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) { 505 | 506 | // Start a new path if current path is closed 507 | if (pathClosed) { 508 | context.beginPath(); 509 | pathClosed = false; 510 | } 511 | 512 | if (layer.autosize) fitRect(x, y, 0, 0); 513 | context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); 514 | 515 | }; 516 | 517 | /** 518 | * Closes the current path by connecting the end point with the start 519 | * point (if any) with a straight line. 520 | */ 521 | this.close = function() { 522 | context.closePath(); 523 | pathClosed = true; 524 | }; 525 | 526 | /** 527 | * Add the specified rectangle to the current path. 528 | * 529 | * @param {Number} x The X coordinate of the upper-left corner of the 530 | * rectangle to draw. 531 | * @param {Number} y The Y coordinate of the upper-left corner of the 532 | * rectangle to draw. 533 | * @param {Number} w The width of the rectangle to draw. 534 | * @param {Number} h The height of the rectangle to draw. 535 | */ 536 | this.rect = function(x, y, w, h) { 537 | 538 | // Start a new path if current path is closed 539 | if (pathClosed) { 540 | context.beginPath(); 541 | pathClosed = false; 542 | } 543 | 544 | if (layer.autosize) fitRect(x, y, w, h); 545 | context.rect(x, y, w, h); 546 | 547 | }; 548 | 549 | /** 550 | * Clip all future drawing operations by the current path. The current path 551 | * is implicitly closed. The current path can continue to be reused 552 | * for other operations (such as fillColor()) but a new path will be started 553 | * once a path drawing operation (path() or rect()) is used. 554 | */ 555 | this.clip = function() { 556 | 557 | // Set new clipping region 558 | context.clip(); 559 | 560 | // Path now implicitly closed 561 | pathClosed = true; 562 | 563 | }; 564 | 565 | /** 566 | * Stroke the current path with the specified color. The current path 567 | * is implicitly closed. The current path can continue to be reused 568 | * for other operations (such as clip()) but a new path will be started 569 | * once a path drawing operation (path() or rect()) is used. 570 | * 571 | * @param {String} cap The line cap style. Can be "round", "square", 572 | * or "butt". 573 | * @param {String} join The line join style. Can be "round", "bevel", 574 | * or "miter". 575 | * @param {Number} thickness The line thickness in pixels. 576 | * @param {Number} r The red component of the color to fill. 577 | * @param {Number} g The green component of the color to fill. 578 | * @param {Number} b The blue component of the color to fill. 579 | * @param {Number} a The alpha component of the color to fill. 580 | */ 581 | this.strokeColor = function(cap, join, thickness, r, g, b, a) { 582 | 583 | // Stroke with color 584 | context.lineCap = cap; 585 | context.lineJoin = join; 586 | context.lineWidth = thickness; 587 | context.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; 588 | context.stroke(); 589 | 590 | // Path now implicitly closed 591 | pathClosed = true; 592 | 593 | }; 594 | 595 | /** 596 | * Fills the current path with the specified color. The current path 597 | * is implicitly closed. The current path can continue to be reused 598 | * for other operations (such as clip()) but a new path will be started 599 | * once a path drawing operation (path() or rect()) is used. 600 | * 601 | * @param {Number} r The red component of the color to fill. 602 | * @param {Number} g The green component of the color to fill. 603 | * @param {Number} b The blue component of the color to fill. 604 | * @param {Number} a The alpha component of the color to fill. 605 | */ 606 | this.fillColor = function(r, g, b, a) { 607 | 608 | // Fill with color 609 | context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")"; 610 | context.fill(); 611 | 612 | // Path now implicitly closed 613 | pathClosed = true; 614 | 615 | }; 616 | 617 | /** 618 | * Stroke the current path with the image within the specified layer. The 619 | * image data will be tiled infinitely within the stroke. The current path 620 | * is implicitly closed. The current path can continue to be reused 621 | * for other operations (such as clip()) but a new path will be started 622 | * once a path drawing operation (path() or rect()) is used. 623 | * 624 | * @param {String} cap The line cap style. Can be "round", "square", 625 | * or "butt". 626 | * @param {String} join The line join style. Can be "round", "bevel", 627 | * or "miter". 628 | * @param {Number} thickness The line thickness in pixels. 629 | * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 630 | * within the stroke. 631 | */ 632 | this.strokeLayer = function(cap, join, thickness, srcLayer) { 633 | 634 | // Stroke with image data 635 | context.lineCap = cap; 636 | context.lineJoin = join; 637 | context.lineWidth = thickness; 638 | context.strokeStyle = context.createPattern( 639 | srcLayer.getCanvas(), 640 | "repeat" 641 | ); 642 | context.stroke(); 643 | 644 | // Path now implicitly closed 645 | pathClosed = true; 646 | 647 | }; 648 | 649 | /** 650 | * Fills the current path with the image within the specified layer. The 651 | * image data will be tiled infinitely within the stroke. The current path 652 | * is implicitly closed. The current path can continue to be reused 653 | * for other operations (such as clip()) but a new path will be started 654 | * once a path drawing operation (path() or rect()) is used. 655 | * 656 | * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern 657 | * within the fill. 658 | */ 659 | this.fillLayer = function(srcLayer) { 660 | 661 | // Fill with image data 662 | context.fillStyle = context.createPattern( 663 | srcLayer.getCanvas(), 664 | "repeat" 665 | ); 666 | context.fill(); 667 | 668 | // Path now implicitly closed 669 | pathClosed = true; 670 | 671 | }; 672 | 673 | /** 674 | * Push current layer state onto stack. 675 | */ 676 | this.push = function() { 677 | 678 | // Save current state onto stack 679 | context.save(); 680 | stackSize++; 681 | 682 | }; 683 | 684 | /** 685 | * Pop layer state off stack. 686 | */ 687 | this.pop = function() { 688 | 689 | // Restore current state from stack 690 | if (stackSize > 0) { 691 | context.restore(); 692 | stackSize--; 693 | } 694 | 695 | }; 696 | 697 | /** 698 | * Reset the layer, clearing the stack, the current path, and any transform 699 | * matrix. 700 | */ 701 | this.reset = function() { 702 | 703 | // Clear stack 704 | while (stackSize > 0) { 705 | context.restore(); 706 | stackSize--; 707 | } 708 | 709 | // Restore to initial state 710 | context.restore(); 711 | context.save(); 712 | 713 | // Clear path 714 | context.beginPath(); 715 | pathClosed = false; 716 | 717 | }; 718 | 719 | /** 720 | * Sets the given affine transform (defined with six values from the 721 | * transform's matrix). 722 | * 723 | * @param {Number} a The first value in the affine transform's matrix. 724 | * @param {Number} b The second value in the affine transform's matrix. 725 | * @param {Number} c The third value in the affine transform's matrix. 726 | * @param {Number} d The fourth value in the affine transform's matrix. 727 | * @param {Number} e The fifth value in the affine transform's matrix. 728 | * @param {Number} f The sixth value in the affine transform's matrix. 729 | */ 730 | this.setTransform = function(a, b, c, d, e, f) { 731 | context.setTransform( 732 | a, b, c, 733 | d, e, f 734 | /*0, 0, 1*/ 735 | ); 736 | }; 737 | 738 | /** 739 | * Applies the given affine transform (defined with six values from the 740 | * transform's matrix). 741 | * 742 | * @param {Number} a The first value in the affine transform's matrix. 743 | * @param {Number} b The second value in the affine transform's matrix. 744 | * @param {Number} c The third value in the affine transform's matrix. 745 | * @param {Number} d The fourth value in the affine transform's matrix. 746 | * @param {Number} e The fifth value in the affine transform's matrix. 747 | * @param {Number} f The sixth value in the affine transform's matrix. 748 | */ 749 | this.transform = function(a, b, c, d, e, f) { 750 | context.transform( 751 | a, b, c, 752 | d, e, f 753 | /*0, 0, 1*/ 754 | ); 755 | }; 756 | 757 | /** 758 | * Sets the channel mask for future operations on this Layer. 759 | * 760 | * The channel mask is a Guacamole-specific compositing operation identifier 761 | * with a single bit representing each of four channels (in order): source 762 | * image where destination transparent, source where destination opaque, 763 | * destination where source transparent, and destination where source 764 | * opaque. 765 | * 766 | * @param {Number} mask The channel mask for future operations on this 767 | * Layer. 768 | */ 769 | this.setChannelMask = function(mask) { 770 | context.globalCompositeOperation = compositeOperation[mask]; 771 | }; 772 | 773 | /** 774 | * Sets the miter limit for stroke operations using the miter join. This 775 | * limit is the maximum ratio of the size of the miter join to the stroke 776 | * width. If this ratio is exceeded, the miter will not be drawn for that 777 | * joint of the path. 778 | * 779 | * @param {Number} limit The miter limit for stroke operations using the 780 | * miter join. 781 | */ 782 | this.setMiterLimit = function(limit) { 783 | context.miterLimit = limit; 784 | }; 785 | 786 | // Initialize canvas dimensions 787 | canvas.width = width; 788 | canvas.height = height; 789 | 790 | // Explicitly render canvas below other elements in the layer (such as 791 | // child layers). Chrome and others may fail to render layers properly 792 | // without this. 793 | canvas.style.zIndex = -1; 794 | 795 | }; 796 | 797 | /** 798 | * Channel mask for the composite operation "rout". 799 | */ 800 | Guacamole.Layer.ROUT = 0x2; 801 | 802 | /** 803 | * Channel mask for the composite operation "atop". 804 | */ 805 | Guacamole.Layer.ATOP = 0x6; 806 | 807 | /** 808 | * Channel mask for the composite operation "xor". 809 | */ 810 | Guacamole.Layer.XOR = 0xA; 811 | 812 | /** 813 | * Channel mask for the composite operation "rover". 814 | */ 815 | Guacamole.Layer.ROVER = 0xB; 816 | 817 | /** 818 | * Channel mask for the composite operation "over". 819 | */ 820 | Guacamole.Layer.OVER = 0xE; 821 | 822 | /** 823 | * Channel mask for the composite operation "plus". 824 | */ 825 | Guacamole.Layer.PLUS = 0xF; 826 | 827 | /** 828 | * Channel mask for the composite operation "rin". 829 | * Beware that WebKit-based browsers may leave the contents of the destionation 830 | * layer where the source layer is transparent, despite the definition of this 831 | * operation. 832 | */ 833 | Guacamole.Layer.RIN = 0x1; 834 | 835 | /** 836 | * Channel mask for the composite operation "in". 837 | * Beware that WebKit-based browsers may leave the contents of the destionation 838 | * layer where the source layer is transparent, despite the definition of this 839 | * operation. 840 | */ 841 | Guacamole.Layer.IN = 0x4; 842 | 843 | /** 844 | * Channel mask for the composite operation "out". 845 | * Beware that WebKit-based browsers may leave the contents of the destionation 846 | * layer where the source layer is transparent, despite the definition of this 847 | * operation. 848 | */ 849 | Guacamole.Layer.OUT = 0x8; 850 | 851 | /** 852 | * Channel mask for the composite operation "ratop". 853 | * Beware that WebKit-based browsers may leave the contents of the destionation 854 | * layer where the source layer is transparent, despite the definition of this 855 | * operation. 856 | */ 857 | Guacamole.Layer.RATOP = 0x9; 858 | 859 | /** 860 | * Channel mask for the composite operation "src". 861 | * Beware that WebKit-based browsers may leave the contents of the destionation 862 | * layer where the source layer is transparent, despite the definition of this 863 | * operation. 864 | */ 865 | Guacamole.Layer.SRC = 0xC; 866 | 867 | /** 868 | * Represents a single pixel of image data. All components have a minimum value 869 | * of 0 and a maximum value of 255. 870 | * 871 | * @constructor 872 | * 873 | * @param {Number} r The red component of this pixel. 874 | * @param {Number} g The green component of this pixel. 875 | * @param {Number} b The blue component of this pixel. 876 | * @param {Number} a The alpha component of this pixel. 877 | */ 878 | Guacamole.Layer.Pixel = function(r, g, b, a) { 879 | 880 | /** 881 | * The red component of this pixel, where 0 is the minimum value, 882 | * and 255 is the maximum. 883 | */ 884 | this.red = r; 885 | 886 | /** 887 | * The green component of this pixel, where 0 is the minimum value, 888 | * and 255 is the maximum. 889 | */ 890 | this.green = g; 891 | 892 | /** 893 | * The blue component of this pixel, where 0 is the minimum value, 894 | * and 255 is the maximum. 895 | */ 896 | this.blue = b; 897 | 898 | /** 899 | * The alpha component of this pixel, where 0 is the minimum value, 900 | * and 255 is the maximum. 901 | */ 902 | this.alpha = a; 903 | 904 | }; 905 | -------------------------------------------------------------------------------- /src/js/guacamole/OutputStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Glyptodon LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | var Guacamole = Guacamole || {}; 24 | 25 | /** 26 | * Abstract stream which can receive data. 27 | * 28 | * @constructor 29 | * @param {Guacamole.Client} client The client owning this stream. 30 | * @param {Number} index The index of this stream. 31 | */ 32 | Guacamole.OutputStream = function(client, index) { 33 | 34 | /** 35 | * Reference to this stream. 36 | * @private 37 | */ 38 | var guac_stream = this; 39 | 40 | /** 41 | * The index of this stream. 42 | * @type Number 43 | */ 44 | this.index = index; 45 | 46 | /** 47 | * Fired whenever an acknowledgement is received from the server, indicating 48 | * that a stream operation has completed, or an error has occurred. 49 | * 50 | * @event 51 | * @param {Guacamole.Status} status The status of the operation. 52 | */ 53 | this.onack = null; 54 | 55 | /** 56 | * Writes the given base64-encoded data to this stream as a blob. 57 | * 58 | * @param {String} data The base64-encoded data to send. 59 | */ 60 | this.sendBlob = function(data) { 61 | client.sendBlob(guac_stream.index, data); 62 | }; 63 | 64 | /** 65 | * Closes this stream. 66 | */ 67 | this.sendEnd = function() { 68 | client.endStream(guac_stream.index); 69 | }; 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /src/js/guacamole/Parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Glyptodon LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | var Guacamole = Guacamole || {}; 24 | 25 | /** 26 | * Simple Guacamole protocol parser that invokes an oninstruction event when 27 | * full instructions are available from data received via receive(). 28 | * 29 | * @constructor 30 | */ 31 | Guacamole.Parser = function() { 32 | 33 | /** 34 | * Reference to this parser. 35 | * @private 36 | */ 37 | var parser = this; 38 | 39 | /** 40 | * Current buffer of received data. This buffer grows until a full 41 | * element is available. After a full element is available, that element 42 | * is flushed into the element buffer. 43 | * 44 | * @private 45 | */ 46 | var buffer = ""; 47 | 48 | /** 49 | * Buffer of all received, complete elements. After an entire instruction 50 | * is read, this buffer is flushed, and a new instruction begins. 51 | * 52 | * @private 53 | */ 54 | var element_buffer = []; 55 | 56 | // The location of the last element's terminator 57 | var element_end = -1; 58 | 59 | // Where to start the next length search or the next element 60 | var start_index = 0; 61 | 62 | /** 63 | * Appends the given instruction data packet to the internal buffer of 64 | * this Guacamole.Parser, executing all completed instructions at 65 | * the beginning of this buffer, if any. 66 | * 67 | * @param {String} packet The instruction data to receive. 68 | */ 69 | this.receive = function(packet) { 70 | 71 | // Truncate buffer as necessary 72 | if (start_index > 4096 && element_end >= start_index) { 73 | 74 | buffer = buffer.substring(start_index); 75 | 76 | // Reset parse relative to truncation 77 | element_end -= start_index; 78 | start_index = 0; 79 | 80 | } 81 | 82 | // Append data to buffer 83 | buffer += packet; 84 | 85 | // While search is within currently received data 86 | while (element_end < buffer.length) { 87 | 88 | // If we are waiting for element data 89 | if (element_end >= start_index) { 90 | 91 | // We now have enough data for the element. Parse. 92 | var element = buffer.substring(start_index, element_end); 93 | var terminator = buffer.substring(element_end, element_end+1); 94 | 95 | // Add element to array 96 | element_buffer.push(element); 97 | 98 | // If last element, handle instruction 99 | if (terminator == ";") { 100 | 101 | // Get opcode 102 | var opcode = element_buffer.shift(); 103 | 104 | // Call instruction handler. 105 | if (parser.oninstruction != null) 106 | parser.oninstruction(opcode, element_buffer); 107 | 108 | // Clear elements 109 | element_buffer.length = 0; 110 | 111 | } 112 | else if (terminator != ',') 113 | throw new Error("Illegal terminator."); 114 | 115 | // Start searching for length at character after 116 | // element terminator 117 | start_index = element_end + 1; 118 | 119 | } 120 | 121 | // Search for end of length 122 | var length_end = buffer.indexOf(".", start_index); 123 | if (length_end != -1) { 124 | 125 | // Parse length 126 | var length = parseInt(buffer.substring(element_end+1, length_end)); 127 | if (isNaN(length)) 128 | throw new Error("Non-numeric character in element length."); 129 | 130 | // Calculate start of element 131 | start_index = length_end + 1; 132 | 133 | // Calculate location of element terminator 134 | element_end = start_index + length; 135 | 136 | } 137 | 138 | // If no period yet, continue search when more data 139 | // is received 140 | else { 141 | start_index = buffer.length; 142 | break; 143 | } 144 | 145 | } // end parse loop 146 | 147 | }; 148 | 149 | /** 150 | * Fired once for every complete Guacamole instruction received, in order. 151 | * 152 | * @event 153 | * @param {String} opcode The Guacamole instruction opcode. 154 | * @param {Array} parameters The parameters provided for the instruction, 155 | * if any. 156 | */ 157 | this.oninstruction = null; 158 | 159 | }; 160 | -------------------------------------------------------------------------------- /src/js/guacamole/Status.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Glyptodon LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | var Guacamole = Guacamole || {}; 24 | 25 | /** 26 | * A Guacamole status. Each Guacamole status consists of a status code, defined 27 | * by the protocol, and an optional human-readable message, usually only 28 | * included for debugging convenience. 29 | * 30 | * @param {Number} code The Guacamole status code, as defined by 31 | * Guacamole.Status.Code. 32 | * @param {String} [message] An optional human-readable message. 33 | */ 34 | Guacamole.Status = function(code, message) { 35 | 36 | /** 37 | * Reference to this Guacamole.Status. 38 | * @private 39 | */ 40 | var guac_status = this; 41 | 42 | /** 43 | * The Guacamole status code. 44 | * @see Guacamole.Status.Code 45 | * @type Number 46 | */ 47 | this.code = code; 48 | 49 | /** 50 | * An arbitrary human-readable message associated with this status, if any. 51 | * The human-readable message is not required, and is generally provided 52 | * for debugging purposes only. For user feedback, it is better to translate 53 | * the Guacamole status code into a message. 54 | * 55 | * @type String 56 | */ 57 | this.message = message; 58 | 59 | /** 60 | * Returns whether this status represents an error. 61 | * @returns {Boolean} true if this status represents an error, false 62 | * otherwise. 63 | */ 64 | this.isError = function() { 65 | return guac_status.code < 0 || guac_status.code > 0x00FF; 66 | }; 67 | 68 | }; 69 | 70 | /** 71 | * Enumeration of all Guacamole status codes. 72 | */ 73 | Guacamole.Status.Code = { 74 | 75 | /** 76 | * The operation succeeded. 77 | * 78 | * @type Number 79 | */ 80 | "SUCCESS": 0x0000, 81 | 82 | /** 83 | * The requested operation is unsupported. 84 | * 85 | * @type Number 86 | */ 87 | "UNSUPPORTED": 0x0100, 88 | 89 | /** 90 | * The operation could not be performed due to an internal failure. 91 | * 92 | * @type Number 93 | */ 94 | "SERVER_ERROR": 0x0200, 95 | 96 | /** 97 | * The operation could not be performed as the server is busy. 98 | * 99 | * @type Number 100 | */ 101 | "SERVER_BUSY": 0x0201, 102 | 103 | /** 104 | * The operation was unsuccessful due to an error or otherwise unexpected 105 | * condition of the upstream server. 106 | * 107 | * @type Number 108 | */ 109 | "UPSTREAM_TIMEOUT": 0x0202, 110 | 111 | /** 112 | * The operation could not be performed because the upstream server is not 113 | * responding. 114 | * 115 | * @type Number 116 | */ 117 | "UPSTREAM_ERROR": 0x0203, 118 | 119 | /** 120 | * The operation could not be performed as the requested resource does not 121 | * exist. 122 | * 123 | * @type Number 124 | */ 125 | "RESOURCE_NOT_FOUND": 0x0204, 126 | 127 | /** 128 | * The operation could not be performed as the requested resource is 129 | * already in use. 130 | * 131 | * @type Number 132 | */ 133 | "RESOURCE_CONFLICT": 0x0205, 134 | 135 | /** 136 | * The operation could not be performed because bad parameters were given. 137 | * 138 | * @type Number 139 | */ 140 | "CLIENT_BAD_REQUEST": 0x0300, 141 | 142 | /** 143 | * Permission was denied to perform the operation, as the user is not yet 144 | * authorized (not yet logged in, for example). 145 | * 146 | * @type Number 147 | */ 148 | "CLIENT_UNAUTHORIZED": 0x0301, 149 | 150 | /** 151 | * Permission was denied to perform the operation, and this permission will 152 | * not be granted even if the user is authorized. 153 | * 154 | * @type Number 155 | */ 156 | "CLIENT_FORBIDDEN": 0x0303, 157 | 158 | /** 159 | * The client took too long to respond. 160 | * 161 | * @type Number 162 | */ 163 | "CLIENT_TIMEOUT": 0x0308, 164 | 165 | /** 166 | * The client sent too much data. 167 | * 168 | * @type Number 169 | */ 170 | "CLIENT_OVERRUN": 0x030D, 171 | 172 | /** 173 | * The client sent data of an unsupported or unexpected type. 174 | * 175 | * @type Number 176 | */ 177 | "CLIENT_BAD_TYPE": 0x030F, 178 | 179 | /** 180 | * The operation failed because the current client is already using too 181 | * many resources. 182 | * 183 | * @type Number 184 | */ 185 | "CLIENT_TOO_MANY": 0x031D 186 | 187 | }; 188 | -------------------------------------------------------------------------------- /src/js/guacamole/StringReader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Glyptodon LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | var Guacamole = Guacamole || {}; 24 | 25 | /** 26 | * A reader which automatically handles the given input stream, returning 27 | * strictly text data. Note that this object will overwrite any installed event 28 | * handlers on the given Guacamole.InputStream. 29 | * 30 | * @constructor 31 | * @param {Guacamole.InputStream} stream The stream that data will be read 32 | * from. 33 | */ 34 | Guacamole.StringReader = function(stream) { 35 | 36 | /** 37 | * Reference to this Guacamole.InputStream. 38 | * @private 39 | */ 40 | var guac_reader = this; 41 | 42 | /** 43 | * Wrapped Guacamole.ArrayBufferReader. 44 | * @private 45 | * @type Guacamole.ArrayBufferReader 46 | */ 47 | var array_reader = new Guacamole.ArrayBufferReader(stream); 48 | 49 | /** 50 | * The number of bytes remaining for the current codepoint. 51 | * 52 | * @type Number 53 | */ 54 | var bytes_remaining = 0; 55 | 56 | /** 57 | * The current codepoint value, as calculated from bytes read so far. 58 | * @type Number 59 | */ 60 | var codepoint = 0; 61 | 62 | /** 63 | * Decodes the given UTF-8 data into a Unicode string. The data may end in 64 | * the middle of a multibyte character. 65 | * 66 | * @private 67 | * @param {ArrayBuffer} buffer Arbitrary UTF-8 data. 68 | * @return {String} A decoded Unicode string. 69 | */ 70 | function __decode_utf8(buffer) { 71 | 72 | var text = ""; 73 | 74 | var bytes = new Uint8Array(buffer); 75 | for (var i=0; i= buffer.length) { 78 | var new_buffer = new Uint8Array((length+bytes)*2); 79 | new_buffer.set(buffer); 80 | buffer = new_buffer; 81 | } 82 | 83 | length += bytes; 84 | 85 | } 86 | 87 | /** 88 | * Appends a single Unicode character to the current buffer, resizing the 89 | * buffer if necessary. The character will be encoded as UTF-8. 90 | * 91 | * @private 92 | * @param {Number} codepoint The codepoint of the Unicode character to 93 | * append. 94 | */ 95 | function __append_utf8(codepoint) { 96 | 97 | var mask; 98 | var bytes; 99 | 100 | // 1 byte 101 | if (codepoint <= 0x7F) { 102 | mask = 0x00; 103 | bytes = 1; 104 | } 105 | 106 | // 2 byte 107 | else if (codepoint <= 0x7FF) { 108 | mask = 0xC0; 109 | bytes = 2; 110 | } 111 | 112 | // 3 byte 113 | else if (codepoint <= 0xFFFF) { 114 | mask = 0xE0; 115 | bytes = 3; 116 | } 117 | 118 | // 4 byte 119 | else if (codepoint <= 0x1FFFFF) { 120 | mask = 0xF0; 121 | bytes = 4; 122 | } 123 | 124 | // If invalid codepoint, append replacement character 125 | else { 126 | __append_utf8(0xFFFD); 127 | return; 128 | } 129 | 130 | // Offset buffer by size 131 | __expand(bytes); 132 | var offset = length - 1; 133 | 134 | // Add trailing bytes, if any 135 | for (var i=1; i>= 6; 138 | } 139 | 140 | // Set initial byte 141 | buffer[offset] = mask | codepoint; 142 | 143 | } 144 | 145 | /** 146 | * Encodes the given string as UTF-8, returning an ArrayBuffer containing 147 | * the resulting bytes. 148 | * 149 | * @private 150 | * @param {String} text The string to encode as UTF-8. 151 | * @return {Uint8Array} The encoded UTF-8 data. 152 | */ 153 | function __encode_utf8(text) { 154 | 155 | // Fill buffer with UTF-8 156 | for (var i=0; i 0) { 163 | var out_buffer = buffer.subarray(0, length); 164 | length = 0; 165 | return out_buffer; 166 | } 167 | 168 | } 169 | 170 | /** 171 | * Sends the given text. 172 | * 173 | * @param {String} text The text to send. 174 | */ 175 | this.sendText = function(text) { 176 | array_writer.sendData(__encode_utf8(text)); 177 | }; 178 | 179 | /** 180 | * Signals that no further text will be sent, effectively closing the 181 | * stream. 182 | */ 183 | this.sendEnd = function() { 184 | array_writer.sendEnd(); 185 | }; 186 | 187 | /** 188 | * Fired for received data, if acknowledged by the server. 189 | * @event 190 | * @param {Guacamole.Status} status The status of the operation. 191 | */ 192 | this.onack = null; 193 | 194 | }; -------------------------------------------------------------------------------- /src/js/guacamole/Tunnel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Glyptodon LLC 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | 23 | var Guacamole = Guacamole || {}; 24 | 25 | /** 26 | * Core object providing abstract communication for Guacamole. This object 27 | * is a null implementation whose functions do nothing. Guacamole applications 28 | * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based 29 | * on this one. 30 | * 31 | * @constructor 32 | * @see Guacamole.HTTPTunnel 33 | */ 34 | Guacamole.Tunnel = function() { 35 | 36 | /** 37 | * Connect to the tunnel with the given optional data. This data is 38 | * typically used for authentication. The format of data accepted is 39 | * up to the tunnel implementation. 40 | * 41 | * @param {String} data The data to send to the tunnel when connecting. 42 | */ 43 | this.connect = function(data) {}; 44 | 45 | /** 46 | * Disconnect from the tunnel. 47 | */ 48 | this.disconnect = function() {}; 49 | 50 | /** 51 | * Send the given message through the tunnel to the service on the other 52 | * side. All messages are guaranteed to be received in the order sent. 53 | * 54 | * @param {...} elements The elements of the message to send to the 55 | * service on the other side of the tunnel. 56 | */ 57 | this.sendMessage = function(elements) {}; 58 | 59 | /** 60 | * The current state of this tunnel. 61 | * 62 | * @type Number 63 | */ 64 | this.state = Guacamole.Tunnel.State.CONNECTING; 65 | 66 | /** 67 | * The maximum amount of time to wait for data to be received, in 68 | * milliseconds. If data is not received within this amount of time, 69 | * the tunnel is closed with an error. The default value is 15000. 70 | * Set to zero for no timeout. 71 | * 72 | * @type Number 73 | */ 74 | this.receiveTimeout = 15000; 75 | 76 | /** 77 | * Fired whenever an error is encountered by the tunnel. 78 | * 79 | * @event 80 | * @param {Guacamole.Status} status A status object which describes the 81 | * error. 82 | */ 83 | this.onerror = null; 84 | 85 | /** 86 | * Fired whenever the state of the tunnel changes. 87 | * 88 | * @event 89 | * @param {Number} state The new state of the client. 90 | */ 91 | this.onstatechange = null; 92 | 93 | /** 94 | * Fired once for every complete Guacamole instruction received, in order. 95 | * 96 | * @event 97 | * @param {String} opcode The Guacamole instruction opcode. 98 | * @param {Array} parameters The parameters provided for the instruction, 99 | * if any. 100 | */ 101 | this.oninstruction = null; 102 | 103 | }; 104 | 105 | /** 106 | * All possible tunnel states. 107 | */ 108 | Guacamole.Tunnel.State = { 109 | 110 | /** 111 | * A connection is in pending. It is not yet known whether connection was 112 | * successful. 113 | * 114 | * @type Number 115 | */ 116 | "CONNECTING": 0, 117 | 118 | /** 119 | * Connection was successful, and data is being received. 120 | * 121 | * @type Number 122 | */ 123 | "OPEN": 1, 124 | 125 | /** 126 | * The connection is closed. Connection may not have been successful, the 127 | * tunnel may have been explicitly closed by either side, or an error may 128 | * have occurred. 129 | * 130 | * @type Number 131 | */ 132 | "CLOSED": 2 133 | 134 | }; 135 | 136 | /** 137 | * Guacamole Tunnel implemented over HTTP via XMLHttpRequest. 138 | * 139 | * @constructor 140 | * @augments Guacamole.Tunnel 141 | * @param {String} tunnelURL The URL of the HTTP tunneling service. 142 | */ 143 | Guacamole.HTTPTunnel = function(tunnelURL) { 144 | 145 | /** 146 | * Reference to this HTTP tunnel. 147 | * @private 148 | */ 149 | var tunnel = this; 150 | 151 | var tunnel_uuid; 152 | 153 | var TUNNEL_CONNECT = tunnelURL + "?connect"; 154 | var TUNNEL_READ = tunnelURL + "?read:"; 155 | var TUNNEL_WRITE = tunnelURL + "?write:"; 156 | 157 | var POLLING_ENABLED = 1; 158 | var POLLING_DISABLED = 0; 159 | 160 | // Default to polling - will be turned off automatically if not needed 161 | var pollingMode = POLLING_ENABLED; 162 | 163 | var sendingMessages = false; 164 | var outputMessageBuffer = ""; 165 | 166 | /** 167 | * The current receive timeout ID, if any. 168 | * @private 169 | */ 170 | var receive_timeout = null; 171 | 172 | /** 173 | * Closes this tunnel, signaling the given status and corresponding 174 | * message, which will be sent to the onerror handler if the status is 175 | * an error status. 176 | * 177 | * @private 178 | * @param {Guacamole.Status} status The status causing the connection to 179 | * close; 180 | */ 181 | function close_tunnel(status) { 182 | 183 | // Ignore if already closed 184 | if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 185 | return; 186 | 187 | // If connection closed abnormally, signal error. 188 | if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) { 189 | 190 | // Ignore RESOURCE_NOT_FOUND if we've already connected, as that 191 | // only signals end-of-stream for the HTTP tunnel. 192 | if (tunnel.state === Guacamole.Tunnel.State.CONNECTING 193 | || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND) 194 | tunnel.onerror(status); 195 | 196 | } 197 | 198 | // Mark as closed 199 | tunnel.state = Guacamole.Tunnel.State.CLOSED; 200 | 201 | // Reset output message buffer 202 | sendingMessages = false; 203 | 204 | if (tunnel.onstatechange) 205 | tunnel.onstatechange(tunnel.state); 206 | 207 | } 208 | 209 | 210 | this.sendMessage = function() { 211 | 212 | // Do not attempt to send messages if not connected 213 | if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 214 | return; 215 | 216 | // Do not attempt to send empty messages 217 | if (arguments.length === 0) 218 | return; 219 | 220 | /** 221 | * Converts the given value to a length/string pair for use as an 222 | * element in a Guacamole instruction. 223 | * 224 | * @private 225 | * @param value The value to convert. 226 | * @return {String} The converted value. 227 | */ 228 | function getElement(value) { 229 | var string = new String(value); 230 | return string.length + "." + string; 231 | } 232 | 233 | // Initialized message with first element 234 | var message = getElement(arguments[0]); 235 | 236 | // Append remaining elements 237 | for (var i=1; i 0) { 259 | 260 | sendingMessages = true; 261 | 262 | var message_xmlhttprequest = new XMLHttpRequest(); 263 | message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); 264 | message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 265 | 266 | // Once response received, send next queued event. 267 | message_xmlhttprequest.onreadystatechange = function() { 268 | if (message_xmlhttprequest.readyState === 4) { 269 | 270 | // If an error occurs during send, handle it 271 | if (message_xmlhttprequest.status !== 200) 272 | handleHTTPTunnelError(message_xmlhttprequest); 273 | 274 | // Otherwise, continue the send loop 275 | else 276 | sendPendingMessages(); 277 | 278 | } 279 | }; 280 | 281 | message_xmlhttprequest.send(outputMessageBuffer); 282 | outputMessageBuffer = ""; // Clear buffer 283 | 284 | } 285 | else 286 | sendingMessages = false; 287 | 288 | } 289 | 290 | function handleHTTPTunnelError(xmlhttprequest) { 291 | 292 | var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code")); 293 | var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message"); 294 | 295 | close_tunnel(new Guacamole.Status(code, message)); 296 | 297 | } 298 | 299 | function handleResponse(xmlhttprequest) { 300 | 301 | var interval = null; 302 | var nextRequest = null; 303 | 304 | var dataUpdateEvents = 0; 305 | 306 | // The location of the last element's terminator 307 | var elementEnd = -1; 308 | 309 | // Where to start the next length search or the next element 310 | var startIndex = 0; 311 | 312 | // Parsed elements 313 | var elements = new Array(); 314 | 315 | function parseResponse() { 316 | 317 | // Do not handle responses if not connected 318 | if (tunnel.state !== Guacamole.Tunnel.State.OPEN) { 319 | 320 | // Clean up interval if polling 321 | if (interval !== null) 322 | clearInterval(interval); 323 | 324 | return; 325 | } 326 | 327 | // Do not parse response yet if not ready 328 | if (xmlhttprequest.readyState < 2) return; 329 | 330 | // Attempt to read status 331 | var status; 332 | try { status = xmlhttprequest.status; } 333 | 334 | // If status could not be read, assume successful. 335 | catch (e) { status = 200; } 336 | 337 | // Start next request as soon as possible IF request was successful 338 | if (!nextRequest && status === 200) 339 | nextRequest = makeRequest(); 340 | 341 | // Parse stream when data is received and when complete. 342 | if (xmlhttprequest.readyState === 3 || 343 | xmlhttprequest.readyState === 4) { 344 | 345 | // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) 346 | if (pollingMode === POLLING_ENABLED) { 347 | if (xmlhttprequest.readyState === 3 && !interval) 348 | interval = setInterval(parseResponse, 30); 349 | else if (xmlhttprequest.readyState === 4 && !interval) 350 | clearInterval(interval); 351 | } 352 | 353 | // If canceled, stop transfer 354 | if (xmlhttprequest.status === 0) { 355 | tunnel.disconnect(); 356 | return; 357 | } 358 | 359 | // Halt on error during request 360 | else if (xmlhttprequest.status !== 200) { 361 | handleHTTPTunnelError(xmlhttprequest); 362 | return; 363 | } 364 | 365 | // Attempt to read in-progress data 366 | var current; 367 | try { current = xmlhttprequest.responseText; } 368 | 369 | // Do not attempt to parse if data could not be read 370 | catch (e) { return; } 371 | 372 | // While search is within currently received data 373 | while (elementEnd < current.length) { 374 | 375 | // If we are waiting for element data 376 | if (elementEnd >= startIndex) { 377 | 378 | // We now have enough data for the element. Parse. 379 | var element = current.substring(startIndex, elementEnd); 380 | var terminator = current.substring(elementEnd, elementEnd+1); 381 | 382 | // Add element to array 383 | elements.push(element); 384 | 385 | // If last element, handle instruction 386 | if (terminator === ";") { 387 | 388 | // Get opcode 389 | var opcode = elements.shift(); 390 | 391 | // Call instruction handler. 392 | if (tunnel.oninstruction) 393 | tunnel.oninstruction(opcode, elements); 394 | 395 | // Clear elements 396 | elements.length = 0; 397 | 398 | } 399 | 400 | // Start searching for length at character after 401 | // element terminator 402 | startIndex = elementEnd + 1; 403 | 404 | } 405 | 406 | // Search for end of length 407 | var lengthEnd = current.indexOf(".", startIndex); 408 | if (lengthEnd !== -1) { 409 | 410 | // Parse length 411 | var length = parseInt(current.substring(elementEnd+1, lengthEnd)); 412 | 413 | // If we're done parsing, handle the next response. 414 | if (length === 0) { 415 | 416 | // Clean up interval if polling 417 | if (!interval) 418 | clearInterval(interval); 419 | 420 | // Clean up object 421 | xmlhttprequest.onreadystatechange = null; 422 | xmlhttprequest.abort(); 423 | 424 | // Start handling next request 425 | if (nextRequest) 426 | handleResponse(nextRequest); 427 | 428 | // Done parsing 429 | break; 430 | 431 | } 432 | 433 | // Calculate start of element 434 | startIndex = lengthEnd + 1; 435 | 436 | // Calculate location of element terminator 437 | elementEnd = startIndex + length; 438 | 439 | } 440 | 441 | // If no period yet, continue search when more data 442 | // is received 443 | else { 444 | startIndex = current.length; 445 | break; 446 | } 447 | 448 | } // end parse loop 449 | 450 | } 451 | 452 | } 453 | 454 | // If response polling enabled, attempt to detect if still 455 | // necessary (via wrapping parseResponse()) 456 | if (pollingMode === POLLING_ENABLED) { 457 | xmlhttprequest.onreadystatechange = function() { 458 | 459 | // If we receive two or more readyState==3 events, 460 | // there is no need to poll. 461 | if (xmlhttprequest.readyState === 3) { 462 | dataUpdateEvents++; 463 | if (dataUpdateEvents >= 2) { 464 | pollingMode = POLLING_DISABLED; 465 | xmlhttprequest.onreadystatechange = parseResponse; 466 | } 467 | } 468 | 469 | parseResponse(); 470 | }; 471 | } 472 | 473 | // Otherwise, just parse 474 | else 475 | xmlhttprequest.onreadystatechange = parseResponse; 476 | 477 | parseResponse(); 478 | 479 | } 480 | 481 | /** 482 | * Arbitrary integer, unique for each tunnel read request. 483 | * @private 484 | */ 485 | var request_id = 0; 486 | 487 | function makeRequest() { 488 | 489 | // Make request, increment request ID 490 | var xmlhttprequest = new XMLHttpRequest(); 491 | xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++)); 492 | xmlhttprequest.send(null); 493 | 494 | return xmlhttprequest; 495 | 496 | } 497 | 498 | this.connect = function(data) { 499 | 500 | // Start tunnel and connect 501 | var connect_xmlhttprequest = new XMLHttpRequest(); 502 | connect_xmlhttprequest.onreadystatechange = function() { 503 | 504 | if (connect_xmlhttprequest.readyState !== 4) 505 | return; 506 | 507 | // If failure, throw error 508 | if (connect_xmlhttprequest.status !== 200) { 509 | handleHTTPTunnelError(connect_xmlhttprequest); 510 | return; 511 | } 512 | 513 | // Get UUID from response 514 | tunnel_uuid = connect_xmlhttprequest.responseText; 515 | 516 | tunnel.state = Guacamole.Tunnel.State.OPEN; 517 | if (tunnel.onstatechange) 518 | tunnel.onstatechange(tunnel.state); 519 | 520 | // Start reading data 521 | handleResponse(makeRequest()); 522 | 523 | }; 524 | 525 | connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true); 526 | connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 527 | connect_xmlhttprequest.send(data); 528 | 529 | }; 530 | 531 | this.disconnect = function() { 532 | close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); 533 | }; 534 | 535 | }; 536 | 537 | Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel(); 538 | 539 | /** 540 | * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest. 541 | * 542 | * @constructor 543 | * @augments Guacamole.Tunnel 544 | * @param {String} tunnelURL The URL of the WebSocket tunneling service. 545 | */ 546 | Guacamole.WebSocketTunnel = function(tunnelURL) { 547 | 548 | /** 549 | * Reference to this WebSocket tunnel. 550 | * @private 551 | */ 552 | var tunnel = this; 553 | 554 | /** 555 | * The WebSocket used by this tunnel. 556 | * @private 557 | */ 558 | var socket = null; 559 | 560 | /** 561 | * The current receive timeout ID, if any. 562 | * @private 563 | */ 564 | var receive_timeout = null; 565 | 566 | /** 567 | * The WebSocket protocol corresponding to the protocol used for the current 568 | * location. 569 | * @private 570 | */ 571 | var ws_protocol = { 572 | "http:": "ws:", 573 | "https:": "wss:" 574 | }; 575 | 576 | // Transform current URL to WebSocket URL 577 | 578 | // If not already a websocket URL 579 | if ( tunnelURL.substring(0, 3) !== "ws:" 580 | && tunnelURL.substring(0, 4) !== "wss:") { 581 | 582 | var protocol = ws_protocol[window.location.protocol]; 583 | 584 | // If absolute URL, convert to absolute WS URL 585 | if (tunnelURL.substring(0, 1) === "/") 586 | tunnelURL = 587 | protocol 588 | + "//" + window.location.host 589 | + tunnelURL; 590 | 591 | // Otherwise, construct absolute from relative URL 592 | else { 593 | 594 | // Get path from pathname 595 | var slash = window.location.pathname.lastIndexOf("/"); 596 | var path = window.location.pathname.substring(0, slash + 1); 597 | 598 | // Construct absolute URL 599 | tunnelURL = 600 | protocol 601 | + "//" + window.location.host 602 | + path 603 | + tunnelURL; 604 | 605 | } 606 | 607 | } 608 | 609 | /** 610 | * Closes this tunnel, signaling the given status and corresponding 611 | * message, which will be sent to the onerror handler if the status is 612 | * an error status. 613 | * 614 | * @private 615 | * @param {Guacamole.Status} status The status causing the connection to 616 | * close; 617 | */ 618 | function close_tunnel(status) { 619 | 620 | // Ignore if already closed 621 | if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 622 | return; 623 | 624 | // If connection closed abnormally, signal error. 625 | if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) 626 | tunnel.onerror(status); 627 | 628 | // Mark as closed 629 | tunnel.state = Guacamole.Tunnel.State.CLOSED; 630 | if (tunnel.onstatechange) 631 | tunnel.onstatechange(tunnel.state); 632 | 633 | socket.close(); 634 | 635 | } 636 | 637 | this.sendRawMessage = function(message) { 638 | socket.send(message); 639 | } 640 | 641 | this.sendMessage = function(elements) { 642 | 643 | // Do not attempt to send messages if not connected 644 | if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 645 | return; 646 | 647 | // Do not attempt to send empty messages 648 | if (arguments.length === 0) 649 | return; 650 | 651 | /** 652 | * Converts the given value to a length/string pair for use as an 653 | * element in a Guacamole instruction. 654 | * 655 | * @private 656 | * @param value The value to convert. 657 | * @return {String} The converted value. 658 | */ 659 | function getElement(value) { 660 | var string = new String(value); 661 | return string.length + "." + string; 662 | } 663 | 664 | // Initialized message with first element 665 | var message = getElement(arguments[0]); 666 | 667 | // Append remaining elements 668 | for (var i=1; i li > div > span.admin { 64 | font-weight: bold; 65 | color: red; 66 | } 67 | .moderator.list-group-item, .message-pane > li > div > span.moderator { 68 | font-weight: bold; 69 | color: green; 70 | } 71 | .dev.list-group-item, .message-pane > li > div > span.dev { 72 | font-weight: bold; 73 | color: blue; 74 | } 75 | .waiting-turn.list-group-item { 76 | background-color: #FFFFB3; 77 | } 78 | .has-turn.list-group-item { 79 | background-color: #CFF2FC; 80 | } 81 | .message-pane-wrapper { 82 | border-radius: 4px; 83 | clear: both; 84 | overflow: auto; 85 | position: absolute; 86 | top: 0; 87 | right: 0; 88 | bottom: 0; 89 | left: 0; 90 | height: auto; 91 | width: auto; 92 | /*margin: 0 200px 31px 0;*/ 93 | margin-bottom: 44px; 94 | background-color: #eee; 95 | font-size: 13px; 96 | padding: 0 5px; 97 | } 98 | 99 | .message-pane { 100 | padding-top: 1px; 101 | } 102 | 103 | .message-pane li { 104 | border-bottom: 1px solid #ccc; 105 | box-shadow: 0 1px 0 0 white; 106 | } 107 | 108 | .message-pane small { 109 | display: none; 110 | font-size: 10px; 111 | position: absolute; 112 | background-color: #f7f7f7; 113 | text-align: center; 114 | line-height: 20px; 115 | margin: 4px 0; 116 | padding: 0 5px; 117 | right: 5px; 118 | } 119 | 120 | .message-pane li:hover { 121 | background-color: #f7f7f7; 122 | } 123 | 124 | .message-pane li:hover small { 125 | display: block; 126 | } 127 | 128 | .message-pane li>div { 129 | overflow: auto; 130 | padding: 2px 0 2px 0; 131 | line-height: 24px; 132 | white-space: -o-pre-wrap; /* Opera */ 133 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 134 | display: inline-block; 135 | } 136 | 137 | .message-pane li>div.server-message { 138 | font-weight: bold; 139 | } 140 | 141 | .message-pane li>div p { 142 | margin: 0; 143 | } 144 | 145 | .message-pane .username { 146 | font-weight: bold; 147 | white-space: nowrap; 148 | display: block; 149 | float: left; 150 | overflow: hidden; 151 | text-align: right; 152 | color: black; 153 | } 154 | 155 | .message-pane .spacer { 156 | font-size: 14px; 157 | margin-right: 5px; 158 | } 159 | /* Guacamole Keyboard */ 160 | .osk { 161 | width: 100%; 162 | position: relative; 163 | } 164 | 165 | .guac-keyboard { 166 | display: inline-block; 167 | width: 100%; 168 | 169 | margin: 0; 170 | padding: 0; 171 | cursor: default; 172 | 173 | text-align: left; 174 | vertical-align: middle; 175 | } 176 | 177 | .guac-keyboard, 178 | .guac-keyboard * { 179 | overflow: hidden; 180 | white-space: nowrap; 181 | } 182 | 183 | .guac-keyboard .guac-keyboard-key-container { 184 | display: inline-block; 185 | margin: 0.05em; 186 | position: relative; 187 | } 188 | 189 | .guac-keyboard .guac-keyboard-key { 190 | 191 | position: absolute; 192 | left: 0; 193 | right: 0; 194 | top: 0; 195 | bottom: 0; 196 | 197 | background: #444; 198 | 199 | border: 0.125em solid #666; 200 | -moz-border-radius: 0.25em; 201 | -webkit-border-radius: 0.25em; 202 | -khtml-border-radius: 0.25em; 203 | border-radius: 0.25em; 204 | 205 | color: white; 206 | font-size: 40%; 207 | font-weight: lighter; 208 | text-align: center; 209 | white-space: pre; 210 | 211 | text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25), 212 | 1px -1px 0 rgba(0, 0, 0, 0.25), 213 | -1px 1px 0 rgba(0, 0, 0, 0.25), 214 | -1px -1px 0 rgba(0, 0, 0, 0.25); 215 | 216 | } 217 | 218 | .guac-keyboard .guac-keyboard-key:hover { 219 | cursor: pointer; 220 | } 221 | 222 | .guac-keyboard .guac-keyboard-key.highlight { 223 | background: #666; 224 | border-color: #666; 225 | } 226 | 227 | /* Align some keys to the left */ 228 | .guac-keyboard .guac-keyboard-key-caps, 229 | .guac-keyboard .guac-keyboard-key-enter, 230 | .guac-keyboard .guac-keyboard-key-tab, 231 | .guac-keyboard .guac-keyboard-key-lalt, 232 | .guac-keyboard .guac-keyboard-key-ralt, 233 | .guac-keyboard .guac-keyboard-key-lctrl, 234 | .guac-keyboard .guac-keyboard-key-rctrl, 235 | .guac-keyboard .guac-keyboard-key-lshift, 236 | .guac-keyboard .guac-keyboard-key-rshift { 237 | text-align: left; 238 | padding-left: 0.75em; 239 | } 240 | 241 | /* Active shift */ 242 | .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-rshift, 243 | .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key-lshift, 244 | 245 | /* Active ctrl */ 246 | .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-rctrl, 247 | .guac-keyboard.guac-keyboard-modifier-control .guac-keyboard-key-lctrl, 248 | 249 | /* Active alt */ 250 | .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-ralt, 251 | .guac-keyboard.guac-keyboard-modifier-alt .guac-keyboard-key-lalt, 252 | 253 | /* Active caps */ 254 | .guac-keyboard.guac-keyboard-modifier-caps .guac-keyboard-key-caps, 255 | 256 | /* Active super */ 257 | .guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-super, 258 | .guac-keyboard.guac-keyboard-modifier-super .guac-keyboard-key-menu { 259 | background: #882; 260 | border-color: #DD4; 261 | } 262 | 263 | .guac-keyboard .guac-keyboard-key.guac-keyboard-disabled { 264 | background: #CCC; 265 | border-color: #BBB; 266 | cursor: default; 267 | } 268 | 269 | .guac-keyboard .guac-keyboard-key.guac-keyboard-pressed { 270 | background: #822; 271 | border-color: #D44; 272 | } 273 | 274 | .guac-keyboard .guac-keyboard-group { 275 | line-height: 0; 276 | } 277 | 278 | .guac-keyboard .guac-keyboard-group.guac-keyboard-alpha, 279 | .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { 280 | display: inline-block; 281 | text-align: center; 282 | vertical-align: top; 283 | } 284 | 285 | .guac-keyboard .guac-keyboard-group.guac-keyboard-main { 286 | 287 | /* IE10 */ 288 | display: -ms-flexbox; 289 | -ms-flex-align: stretch; 290 | -ms-flex-direction: row; 291 | 292 | /* Ancient Mozilla */ 293 | display: -moz-box; 294 | -moz-box-align: stretch; 295 | -moz-box-orient: horizontal; 296 | 297 | /* Ancient WebKit */ 298 | display: -webkit-box; 299 | -webkit-box-align: stretch; 300 | -webkit-box-orient: horizontal; 301 | 302 | /* Old WebKit */ 303 | display: -webkit-flex; 304 | -webkit-align-items: stretch; 305 | -webkit-flex-direction: row; 306 | 307 | /* W3C */ 308 | display: flex; 309 | align-items: stretch; 310 | flex-direction: row; 311 | 312 | } 313 | 314 | .guac-keyboard .guac-keyboard-group.guac-keyboard-movement { 315 | -ms-flex: 1 1 auto; 316 | -moz-box-flex: 1; 317 | -webkit-box-flex: 1; 318 | -webkit-flex: 1 1 auto; 319 | flex: 1 1 auto; 320 | } 321 | 322 | .guac-keyboard .guac-keyboard-gap { 323 | display: inline-block; 324 | } 325 | 326 | /* Hide keycaps requiring modifiers which are NOT currently active. */ 327 | .guac-keyboard:not(.guac-keyboard-modifier-caps) 328 | .guac-keyboard-cap.guac-keyboard-requires-caps, 329 | 330 | .guac-keyboard:not(.guac-keyboard-modifier-shift) 331 | .guac-keyboard-cap.guac-keyboard-requires-shift, 332 | 333 | /* Hide keycaps NOT requiring modifiers which ARE currently active, where that 334 | modifier is used to determine which cap is displayed for the current key. */ 335 | .guac-keyboard.guac-keyboard-modifier-shift 336 | .guac-keyboard-key.guac-keyboard-uses-shift 337 | .guac-keyboard-cap:not(.guac-keyboard-requires-shift), 338 | 339 | .guac-keyboard.guac-keyboard-modifier-caps 340 | .guac-keyboard-key.guac-keyboard-uses-caps 341 | .guac-keyboard-cap:not(.guac-keyboard-requires-caps) { 342 | 343 | display: none; 344 | 345 | } 346 | 347 | /* Spinner */ 348 | .sk-fading-circle { 349 | width: 100%; 350 | height: 100%; 351 | position: relative; 352 | } 353 | 354 | .sk-fading-circle .sk-circle { 355 | width: 100%; 356 | height: 100%; 357 | position: absolute; 358 | left: 0; 359 | top: 0; 360 | } 361 | 362 | .sk-fading-circle .sk-circle:before { 363 | content: ''; 364 | display: block; 365 | margin: 0 auto; 366 | width: 15%; 367 | height: 15%; 368 | background-color: #333; 369 | border-radius: 100%; 370 | -webkit-animation: sk-circleFadeDelay 1.2s infinite ease-in-out both; 371 | animation: sk-circleFadeDelay 1.2s infinite ease-in-out both; 372 | } 373 | .sk-fading-circle .sk-circle2 { 374 | -webkit-transform: rotate(30deg); 375 | -ms-transform: rotate(30deg); 376 | transform: rotate(30deg); 377 | } 378 | .sk-fading-circle .sk-circle3 { 379 | -webkit-transform: rotate(60deg); 380 | -ms-transform: rotate(60deg); 381 | transform: rotate(60deg); 382 | } 383 | .sk-fading-circle .sk-circle4 { 384 | -webkit-transform: rotate(90deg); 385 | -ms-transform: rotate(90deg); 386 | transform: rotate(90deg); 387 | } 388 | .sk-fading-circle .sk-circle5 { 389 | -webkit-transform: rotate(120deg); 390 | -ms-transform: rotate(120deg); 391 | transform: rotate(120deg); 392 | } 393 | .sk-fading-circle .sk-circle6 { 394 | -webkit-transform: rotate(150deg); 395 | -ms-transform: rotate(150deg); 396 | transform: rotate(150deg); 397 | } 398 | .sk-fading-circle .sk-circle7 { 399 | -webkit-transform: rotate(180deg); 400 | -ms-transform: rotate(180deg); 401 | transform: rotate(180deg); 402 | } 403 | .sk-fading-circle .sk-circle8 { 404 | -webkit-transform: rotate(210deg); 405 | -ms-transform: rotate(210deg); 406 | transform: rotate(210deg); 407 | } 408 | .sk-fading-circle .sk-circle9 { 409 | -webkit-transform: rotate(240deg); 410 | -ms-transform: rotate(240deg); 411 | transform: rotate(240deg); 412 | } 413 | .sk-fading-circle .sk-circle10 { 414 | -webkit-transform: rotate(270deg); 415 | -ms-transform: rotate(270deg); 416 | transform: rotate(270deg); 417 | } 418 | .sk-fading-circle .sk-circle11 { 419 | -webkit-transform: rotate(300deg); 420 | -ms-transform: rotate(300deg); 421 | transform: rotate(300deg); 422 | } 423 | .sk-fading-circle .sk-circle12 { 424 | -webkit-transform: rotate(330deg); 425 | -ms-transform: rotate(330deg); 426 | transform: rotate(330deg); 427 | } 428 | .sk-fading-circle .sk-circle2:before { 429 | -webkit-animation-delay: -1.1s; 430 | animation-delay: -1.1s; 431 | } 432 | .sk-fading-circle .sk-circle3:before { 433 | -webkit-animation-delay: -1s; 434 | animation-delay: -1s; 435 | } 436 | .sk-fading-circle .sk-circle4:before { 437 | -webkit-animation-delay: -0.9s; 438 | animation-delay: -0.9s; 439 | } 440 | .sk-fading-circle .sk-circle5:before { 441 | -webkit-animation-delay: -0.8s; 442 | animation-delay: -0.8s; 443 | } 444 | .sk-fading-circle .sk-circle6:before { 445 | -webkit-animation-delay: -0.7s; 446 | animation-delay: -0.7s; 447 | } 448 | .sk-fading-circle .sk-circle7:before { 449 | -webkit-animation-delay: -0.6s; 450 | animation-delay: -0.6s; 451 | } 452 | .sk-fading-circle .sk-circle8:before { 453 | -webkit-animation-delay: -0.5s; 454 | animation-delay: -0.5s; 455 | } 456 | .sk-fading-circle .sk-circle9:before { 457 | -webkit-animation-delay: -0.4s; 458 | animation-delay: -0.4s; 459 | } 460 | .sk-fading-circle .sk-circle10:before { 461 | -webkit-animation-delay: -0.3s; 462 | animation-delay: -0.3s; 463 | } 464 | .sk-fading-circle .sk-circle11:before { 465 | -webkit-animation-delay: -0.2s; 466 | animation-delay: -0.2s; 467 | } 468 | .sk-fading-circle .sk-circle12:before { 469 | -webkit-animation-delay: -0.1s; 470 | animation-delay: -0.1s; 471 | } 472 | 473 | @-webkit-keyframes sk-circleFadeDelay { 474 | 0%, 39%, 100% { opacity: 0; } 475 | 40% { opacity: 1; } 476 | } 477 | 478 | @keyframes sk-circleFadeDelay { 479 | 0%, 39%, 100% { opacity: 0; } 480 | 40% { opacity: 1; } 481 | } 482 | 483 | #vm-list { 484 | display: flex; 485 | flex-wrap: wrap; 486 | } 487 | -------------------------------------------------------------------------------- /src/res/notify.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/res/notify.m4a -------------------------------------------------------------------------------- /src/res/notify.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/res/notify.mp3 -------------------------------------------------------------------------------- /src/res/notify.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computernewb/collab-vm-classic-web-app/24ec00a270ef25f8792e3b4cd4a73d19c5e89629/src/res/notify.ogg -------------------------------------------------------------------------------- /src/templates/navbar.html: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /src/templates/win-logo-ascii.html: -------------------------------------------------------------------------------- 1 | 66 | 67 | --------------------------------------------------------------------------------