107 Commits

Author SHA1 Message Date
Pierre Ossman
aead0b2f89 noVNC 1.5.0 beta 2024-06-03 14:46:13 +02:00
Pierre Ossman
68e09ee8b3 Upgrade to websockify 0.12.0 in snap package 2024-06-03 14:45:39 +02:00
Pierre Ossman
f28e9daec3 Update translation template file 2024-06-03 14:10:47 +02:00
Pierre Ossman
fc11b9d2b0 Remove Twitter links
These are not updated anymore as they are not under the control of the
current team.
2024-06-03 14:09:00 +02:00
Pierre Ossman
d80e3bfa2f Add unit tests for Tight gradient filter 2024-05-16 16:53:49 +02:00
Jiang XueQian
c187b2e5e0 Implement gradient filter of tight decoder, fixing issue #1767
This commit is a basic implementation of the gradient filter required by
qemu `lossy` option.
2024-05-02 20:41:38 +08:00
Samuel Mannehed
10ee10ce56 Cleanup "no-console" eslint rules
Removes unexpected exceptions and clarifies where we want to avoid
console calls.
2024-04-30 15:26:50 +02:00
Samuel Mannehed
8d1b665808 Migrate deprecated eslint config to to new format
The .eslintrc and .eslintignore formats are deprecated. The new format
uses a single eslint.config.js (or .mjs) file at the top.
2024-04-30 15:26:50 +02:00
Samuel Mannehed (ThinLinc team)
c998c723ad Merge pull request #1853 from kosmasgiannis/gr20240424
Updated greek translations
2024-04-26 09:41:25 +02:00
Giannis Kosmas
9d293f1aba Updated el.po 2024-04-24 19:53:49 +03:00
Bubble
92c8a91964 Update zh_CN.po (#1851)
Update Chinese translation
2024-04-24 16:54:24 +02:00
Kostiantyn Syrykh
9a1b1f0d06 Clipboard: handle multiple CR+LF 2024-03-25 17:35:28 +02:00
Samuel Mannehed (ThinLinc team)
786aba602f Merge pull request #1834 from sbungartz/avoid-exception-when-disconnecting-after-dom-morph
Avoid exception when cursor was removed from DOM already
2024-02-22 16:26:51 +01:00
Pierre Ossman
65e9ecd5af Merge branch 'actions' of github.com:CendioOssman/noVNC 2024-02-05 16:58:00 +01:00
Pierre Ossman
cd927723bc Fix import of "commander"
The default import was deprecated ages ago, and in v12 it has now
finally been changed in a breaking way.

Change the code to import things the proper way.
2024-02-05 16:53:20 +01:00
Pierre Ossman
60643fe695 Update github actions to latest versions
Primarily to avoid the versions that are now deprecated, but also update
actions/upload-artifact to keep us up to date.
2024-02-05 16:34:47 +01:00
Samuel Mannehed
e75938bebc Make non-HTTPS message more harsh
As browsers are placing more and more new functionality as
secure-context only, we need to prepare users for more problems. I find
it likely that we will disable non-HTTPS connections in the future.
2024-02-02 16:51:21 +01:00
Simon Bungartz
bd32922ff8 Avoid exception when cursor was removed from DOM already 2024-01-31 16:16:01 +00:00
Pierre Ossman
48c8e41877 Fix key event debug output
Fix for a0b7c0dac5.
2024-01-23 12:54:18 +01:00
Pierre Ossman
ab2fd41693 Handle broken Oculus browser keyboard events
It sets KeyboardEvent.key to "Unidentified" for all non-character keys,
which means we must ignore it and use the legacy handling to figure out
the key pressed.
2024-01-23 12:51:35 +01:00
Pierre Ossman
fca48df85d Increase test timeout for Chrome on Windows
There is some bug in Chrome 119+ on some systems, where it takes
forever for the first readback from a canvas, timing out the first
test that does this.

Work around the issue by increasing the timeout on that platform until
Chrome manages to resolve the issue.
2024-01-19 16:19:41 +01:00
Pierre Ossman
b35cf6dd12 Don't include ES6 module versions in npm package
The npm package is supposed to be for CommonJS usage, so only package
that to avoid confusion. This has become an issue now that nodejs
supports ES6 modules, where users are accidentally trying to import the
wrong files and get errors.
2024-01-17 16:19:16 +01:00
Pierre Ossman
d3aaf4d5b3 Upgrade base snap to Ubuntu 22.04
Ubuntu 18.04 base snap is no longer supported, so switch to the
currently newest one.
2024-01-10 14:44:44 +01:00
Pierre Ossman
796e924e47 Remove unused npm dependencies
These should have been removed as part of 890cff9.
2024-01-10 14:41:52 +01:00
Pierre Ossman
829725b30e Handle relative paths in novnc_proxy
websockify changes the working directory before it starts looking for
files, so we must give it relative paths for things to work reliably.
2023-12-05 11:33:15 +01:00
Pierre Ossman
9ac632deee Handle immediate connection errors
The browser might throw an exception right away if there is something it
doesn't like with our connect attempt. E.g. using a non-TLS WebSocket
from a TLS web page.
2023-12-05 11:30:30 +01:00
Pierre Ossman
7d2dad0f9e Merge branch 'listen-host' of https://github.com/afbjorklund/noVNC 2023-10-27 12:57:09 +02:00
Anders F Björklund
5ebc297164 Remove comment about websockify command arguments 2023-10-14 09:55:25 +02:00
Anders F Björklund
a792b7f39e Document default port applies to all interfaces 2023-10-14 09:55:25 +02:00
Anders F Björklund
f1174023c1 Add the possibility to listen on a specific host
For instance, for listening only on "localhost"

That is, bind on 127.0.0.1 instead of 0.0.0.0
2023-10-14 09:55:24 +02:00
Pierre Ossman
85a465288b Merge branch 'qemu_ledstate_pseudo_encoding' of https://github.com/otthou/noVNC 2023-09-29 14:18:02 +02:00
Otto van Houten
a0b7c0dac5 Add QEMU Led Pseudo encoding support
Previously, num-lock and caps-lock syncing was performed on a best effort basis by qemu.
Now, the syncing is performed by no-vnc instead. This allows the led state syncing to work
in cases where it previously couldn't, since no-vnc has with this extension knowledge of both
the remote and local capslock and numlock status, which QEMU doesn't have.
2023-09-29 13:58:55 +02:00
Pierre Ossman
bf12c24f4c Fix bad indentation 2023-09-07 15:35:20 +02:00
Pierre Ossman
370f21b117 Correctly handle legacy security rejections
The code comment of this code was entirely incorrect, but the commit
message for 5671072 when it was added was correct. I.e. there is a
result, but not a reason.

Adjust the unit tests to make sure this doesn't regress again.
2023-09-07 14:59:36 +02:00
Pierre Ossman
72f6810797 Correctly handle "none" auth on old servers
There is no security result for the "none" authentication until RFB 3.8.
This got broken by mistake in 5671072.
2023-09-07 14:38:04 +02:00
Pierre Ossman
e81602d705 Fix zlib level change in clipboard tests
The compression level got changed in 01bb36d4, but the tests weren't
updated to follow this change.
2023-08-29 17:38:44 +02:00
Pierre Ossman
b40a45a11b Remove unused argument to inflateInit()
There is just one argument to inflateInit(). It is inflateInit2() that
takes two arguments.

Since this argument was never used, let's just remove it and keep the
existing behaviour.
2023-08-29 17:30:00 +02:00
Pierre Ossman
01bb36d431 Use proper argument to deflateInit()
This was an accidental copy error from inflator.js. The second argument
to deflateInit() is the compression level, not the window bits.

We have not strong opinions on an appropriate level, so stick to the
default.
2023-08-29 17:28:54 +02:00
Samuel Mannehed
295004cabe Merge pull request #1710 from novnc/localStorage
Don't crash if we can't use localStorage
2023-07-18 09:42:01 +02:00
Samuel Mannehed
a30f609de4 Don't crash if we can't use localStorage
Our settings are not a fatal requirement, we can fall back on the
default values if they can't be accessed. A scenario where we've seen
this happen is when cookies are disabled in the browser. It seems
localStorage is disabled along with cookies in these settings.

So, lets log a message about the failure and otherwise silently
continue in this case.

Fixes issue #1577.
2023-07-13 14:35:07 +02:00
Pierre Ossman
ca6527c1bf Merge branch 'websock' of https://github.com/CendioOssman/noVNC 2023-06-30 18:20:44 +02:00
Pierre Ossman
ccef89f556 Implicitly flush Websock if needed
Callers shouldn't have to deal with the internal buffering limits of
Websock, so implicitly flush the buffer if more room is needed.
2023-06-04 22:32:43 +02:00
Pierre Ossman
f8b65f9fe1 Add Websock send queue helpers
Callers shouldn't be poking around directly in to the send queue, but
should use accessor functions like for the read queue.
2023-06-04 22:32:43 +02:00
Pierre Ossman
7356d4e60b Move WebSocket queue index reset to receive
It's more robust to do this just before we need the space, rather than
assume when the queue will be read and adjust things right after.
2023-06-04 22:32:43 +02:00
Pierre Ossman
3fc0cb0cb7 Remove Base64 WebSocket remnants
There is no encoding/decoding in modern WebSockets, so let's clean up
some of the old crud that no longer serves a purpose.
2023-06-04 22:32:43 +02:00
Pierre Ossman
b146de6d69 Avoid internal variables in recv queue tests
Makes for more robust and realistic tests.
2023-06-04 22:32:43 +02:00
Pierre Ossman
b298bf9e90 Don't split large WebSocket data in tests
It takes too much time and can make the tests fail.
2023-06-04 22:32:43 +02:00
Pierre Ossman
2a7db6f647 Make ExtendedDesktopSize tests more realistic
Send real messages and avoid poking around in internals, as we weren't
testing things correctly that way.
2023-06-04 19:12:02 +02:00
Pierre Ossman
45cedea78f Don't send SetDesktopSize too early
We don't know the server layout yet, so we can't preserve the screen id
or flags yet at this point. Move it until after we've parsed everything.
2023-06-04 19:12:02 +02:00
Pierre Ossman
12d2e7832d Properly decode ExtendedDesktopSize fields
We are expected to preserve these and use them in our requests back to
the server. We can't do that if we don't actually decode them correctly.
2023-06-04 19:12:02 +02:00
Pierre Ossman
0ccc679d32 Return unsigned values from rQshift32()
This is what we almost always want, and this makes it consistent with
rQshift8() and rQshift16().
2023-06-04 19:12:02 +02:00
Pierre Ossman
d0203a5995 Always return copy of data from socket
We don't know how long the caller will hang on to this data, so we need
to be safe by default and assume it will kept indefinitely. That means
we can't return a reference to the internal buffer, as that will get
overwritten with future messages.

We want to avoid unnecessary copying in performance critical code,
though. So allow code to explicitly ask for a shared buffer, assuming
they know the data needs to be consumed immediately.
2023-06-04 19:12:02 +02:00
Pierre Ossman
aaa4eb8c3c Use proper socket helpers for FBU header
Let's not duplicate this stuff when we have convenience functions.
2023-06-04 19:00:33 +02:00
Pierre Ossman
e01dd27be4 Remove Websock implicit read length
Callers should be properly aware of how much data they need, as they
need to call rQwait() first to ensure the data is present.
2023-06-04 19:00:33 +02:00
Pierre Ossman
55ffe8fc51 Stop exposing Websock queue length
Callers should be using rQwait() to ensure sufficient data is present,
and not poke around in the internal buffering.
2023-06-04 19:00:33 +02:00
Pierre Ossman
0180bc81c1 Stop direct access to socket buffer
Use proper accessor functions instead of poking around in internal
buffers.
2023-06-04 19:00:33 +02:00
Pierre Ossman
fb3c8f64e9 Switch Display.flush() to use a promise
That is the modern way to handle operations that cannot complete
immediately.
2023-06-04 19:00:33 +02:00
Pierre Ossman
ae9b042df1 Change rQslice() to rQpeekBytes()
We don't need any full slice functionality, so let's change this to
better march rQpeek8() and rQshiftBytes().
2023-06-04 19:00:33 +02:00
Pierre Ossman
87143b361e Reduce kept state in JPEG decoder
We don't have to keep track of this much data between rects, so
restructure things to make it more simple. This allows the JPEG parsing
code to be a pure function which only depends on the input.
2023-06-04 19:00:33 +02:00
Pierre Ossman
e8ad466e45 Merge branch 'testsfix' of github.com:CendioOssman/noVNC 2023-06-04 18:59:38 +02:00
Pierre Ossman
eb0ad829d2 Check that decoders consume all data
This is extra important in the tests where we expect no changes to the
display, as otherwise we can't tell the difference between success and a
decoder that is simply waiting for more data.
2023-06-03 15:36:29 +02:00
Pierre Ossman
d33f5ce77f Make extended clipboard tests independent
Let's test the full final result instead of assuming specific internal
calls.
2023-05-30 20:48:26 +02:00
Pierre Ossman
8ae789daf0 Add missing tests for message encodings
All of these functions should have units tests, even if they are fairly
minimal.
2023-05-30 20:48:26 +02:00
Pierre Ossman
9e02f4d01d Return a copy of the data from FakeWebSocket
The caller might hang on to the data for multiple calls, so we make sure
the returned buffer might not get overwritten.
2023-05-30 20:48:26 +02:00
Pierre Ossman
9c7576a587 Remove bad Websock mock in tests
This small object will not properly fake a Websock in more complex
cases, so let's avoid it and create a real Websock instead.
2023-05-30 20:48:26 +02:00
Pierre Ossman
e07ca6a8e2 Fix Websock send tests
Avoid poking around in the internals and instead test what is actually
sent out on the WebSocket.
2023-05-30 20:48:24 +02:00
Pierre Ossman
336ec86997 Remove internal monitoring from Plain tests
Tests should avoid poking in to the internals and should only look at
external behaviour.
2023-05-30 20:11:51 +02:00
Pierre Ossman
0c80c68e92 Avoid hooking in to RFB._fail for tests
This is an internal function so we should not be examining it in the
tests. Instead use the well defined public APIs to check for correct
behaviour.
2023-05-30 20:11:51 +02:00
Pierre Ossman
13fa6b5908 Fix last rect test
Avoid poking in to internals and instead test that the RFB object
responds correctly to new messages.
2023-05-30 20:11:51 +02:00
Pierre Ossman
549ccc7121 Split RSA-AES test data
Make the tests more clear what data is expected in the different stages
of the handshake.
2023-05-30 20:11:51 +02:00
Pierre Ossman
42bc251eb4 Make RSA-AES tests more asynchronous
The code tested here makes heavy use of promises, so it is easier to
test things also using promise centric code.
2023-05-30 20:11:51 +02:00
Pierre Ossman
afbb1da4d5 Remove custom RSA-AES event
We shouldn't add extra, undocumented, API just for the tests. They need
to figure out less invasive way to probe things.
2023-05-30 20:11:51 +02:00
Pierre Ossman
458405e05d Merge RSA-AES tests in to RFB tests
These test the RFB class, so they should be with all other tests for
that class.
2023-05-30 20:11:51 +02:00
Pierre Ossman
0ee0e96f34 Fix ARD authentication test to send real data
Stop bypassing the data handling steps in the test as that means those
parts don't get tested.
2023-05-30 20:11:51 +02:00
Pierre Ossman
71bb6f02cd Fix Plain authentication test checks
We should have constants local for the test function when doing
comparisons or we might have false positives because we compare with
buggy values in the code under test.
2023-05-30 20:11:51 +02:00
Pierre Ossman
79f099108f Split Plain authentication tests to own section
VeNCrypt is a multiplexer for many authentication methods, not just
Plain. So let's split it to its own section, just like other types.
2023-05-30 20:11:51 +02:00
Pierre Ossman
0679c8a801 Test credentials using normal API
Avoid poking around in the internals and instead test things using the
official methods and events. This should give us more realistic and
robust tests.
2023-05-30 20:11:51 +02:00
Pierre Ossman
29a50620ff Avoid touching internals in Tight auth tests
We should test using only external manipulation so we don't assume a
specific implementation.
2023-05-30 20:11:51 +02:00
Pierre Ossman
c7c293279b Remove commented out Tight test case
This is not something we intend to implement, so remove this never used
test case.
2023-05-30 20:11:51 +02:00
Pierre Ossman
cd231e53ed Don't overwrite methods with spies
Spies should just attach without modifying the real method, or we might
get unwanted side effects.
2023-05-30 20:11:51 +02:00
Pierre Ossman
3ef57d1600 Fix security to authentication state test
The "None" authentication will directly progress past authentication, so
it's not a good type for this test.
2023-05-30 20:11:51 +02:00
Pierre Ossman
da75689f4c Fix data for empty RRE rect test
The given data was not a correct RRE rect.
2023-05-30 20:11:51 +02:00
Pierre Ossman
9b115a4485 Send ArrayBuffer, not Uint8Array in tests
This matches the true contents of a WebSocket 'message' event, so should
be a more realistic test.
2023-05-30 20:11:51 +02:00
Pierre Ossman
775ccaa74c Handle immediate responses in RSA-AES authentication
The event handlers might provide the needed response right away, before
even existing the event handler. We should be prepared to handle this
case.
2023-05-30 20:11:51 +02:00
Pierre Ossman
0dd9678e64 Harmonise extended clipboard tests
Let them all follow the same pattern to make things more clear.
2023-05-30 20:11:51 +02:00
Pierre Ossman
91307951d3 Fix cached JPEG test
This test didn't really check anything useful as the end result would be
the same if the second JPEG failed to render.

Fix this by clearing the canvas between the images, so we can tell if
the second image actually rendered or not.
2023-05-30 20:11:51 +02:00
Pierre Ossman
c2d6a06d6d Merge branch 'master' of https://github.com/lewayotte/noVNC 2023-05-11 12:20:18 +02:00
Pierre Ossman
a565ae559f Merge branches 'l10n' and 'fragment' of github.com:CendioOssman/noVNC 2023-05-10 13:23:34 +02:00
Pierre Ossman
0374b4c0fc Handle translation loading in translation class
Let's try to keep as much as possible of the translation handling in a
single place for clarity.
2023-05-10 13:11:51 +02:00
Pierre Ossman
cd1a63b737 Restore history state after tests
We don't want to mess up anything permanent in each test or the tests
might start affecting each other.
2023-05-10 12:25:46 +02:00
Pierre Ossman
05b6d2ad67 Fix typos in query variable comment 2023-05-10 12:25:46 +02:00
Pierre Ossman
2a21bee245 Revert broken Add support for URL fragment parameters
This is a revert of the code changes in commit
f796b05e42 as it served no functional
purpose.

Fragments were already respected for setting parameters, via a different
function. Thus it is unclear what that commit tried to fix. It also
complicated things by mixing the document location with the window
location.

The comment changes are useful, though, so those are kept.
2023-05-10 12:25:46 +02:00
Samuel Mannehed
cbbd9ab069 Merge pull request #1777 from nwtgck/patch-1
fix typo
2023-04-30 02:36:05 +02:00
Ryo Ota
2a675b3394 fix typo 2023-04-30 02:04:00 +09:00
Lew Ayotte
b16f19f9ce Set _rfbVeNCryptState = 4 not == 4 2023-04-06 15:00:21 -05:00
Pierre Ossman
681632bc9f Avoid running tests on l10n singleton
We want tests to be independent, so we cannot have them modify a shared
state, such as the l10n singleton. Make sure each test instantiates its
own object instead.
2023-04-06 11:11:47 +02:00
Pierre Ossman
a4453c9a26 Special case English translation fallback
We should not be listing this in LINGUAS as that gives the impression
that English has en explicit translation. Instead, it is a special case
that the code needs to be explicitly aware of.

This reverts 9a06058 in favour of a more robust fix.
2023-04-05 12:46:17 +02:00
Pierre Ossman
747603c0d5 Also re-enable playback on failures
Allows easier testing by being able to run the test multiple times.
2023-04-04 17:02:57 +02:00
Pierre Ossman
c1d2449fb8 Fix playback error message for load failure
We expect the promise to be rejected with a string for display to the
user.
2023-04-04 17:02:21 +02:00
Samuel Mannehed
46292477c8 Merge pull request #1768 from mathis-marcotte/add-english-to-supported-langs
Add english to list of supported languages
2023-04-02 20:16:48 +02:00
Mathis Marcotte
9a06058f66 Added english to list of supported languages 2023-03-27 14:23:09 +00:00
Samuel Mannehed
8decca7353 Use unitless numbers for line-height values
Using <length> type values for line-height can give unexpected
inheritance behaviors. If using <length> values, the inherited
line-height on children is calculated using the font-size of the parent.
What we want is for the line-height of children to be calculated using
it's own font-size.

By instead using a unitless number, we get the behavior we want. Note
that this bug has no effects right now since no children to any of the
related elements have different font-sizes.
2023-03-23 11:06:03 +01:00
Samuel Mannehed
4558104196 Properly center the checkbox checkmark
Using a flexbox we can easily center the checkmark without using hard
coded positions.
2023-03-23 11:06:03 +01:00
NNN1590
6751cc1236 Update Japanese translation 2023-03-21 13:15:26 +09:00
Pierre Ossman
9985950bfa Upgrade to latest websockify in snap package 2023-01-26 10:45:26 +01:00
Pierre Ossman
a0e6e7b1d8 Merge branch 'crypto-cleanup-fallback' of https://github.com/pdlan/noVNC 2023-01-20 16:52:32 +01:00
pdlan
f974b73137 Cleanup for the cryptographic algorithms that are not supported by SubtleCrypto 2023-01-20 05:54:00 -05:00
73 changed files with 3957 additions and 2544 deletions

View File

@@ -1 +0,0 @@
**/xtscancodes.js

View File

@@ -1,54 +0,0 @@
{
"env": {
"browser": true,
"es2020": true
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2020
},
"extends": "eslint:recommended",
"rules": {
// Unsafe or confusing stuff that we forbid
"no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }],
"no-constant-condition": ["error", { "checkLoops": false }],
"no-var": "error",
"no-useless-constructor": "error",
"object-shorthand": ["error", "methods", { "avoidQuotes": true }],
"prefer-arrow-callback": "error",
"arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": false } ],
"arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }],
"arrow-spacing": ["error"],
"no-confusing-arrow": ["error", { "allowParens": true }],
// Enforced coding style
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"indent": ["error", 4, { "SwitchCase": 1,
"VariableDeclarator": "first",
"FunctionDeclaration": { "parameters": "first" },
"FunctionExpression": { "parameters": "first" },
"CallExpression": { "arguments": "first" },
"ArrayExpression": "first",
"ObjectExpression": "first",
"ImportDeclaration": "first",
"ignoreComments": true }],
"comma-spacing": ["error"],
"comma-style": ["error"],
"curly": ["error", "multi-line"],
"func-call-spacing": ["error"],
"func-names": ["error"],
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"key-spacing": ["error"],
"keyword-spacing": ["error"],
"no-trailing-spaces": ["error"],
"semi": ["error"],
"space-before-blocks": ["error"],
"space-before-function-paren": ["error", { "anonymous": "always",
"named": "never",
"asyncArrow": "always" }],
"switch-colon-spacing": ["error"],
"camelcase": ["error", { allow: ["^XK_", "^XF86XK_"] }],
}
}

View File

@@ -10,18 +10,18 @@ jobs:
npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
if: github.event_name != 'release'
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
# Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org'
- run: npm install
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: npm
path: lib
@@ -49,7 +49,7 @@ jobs:
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
@@ -61,7 +61,7 @@ jobs:
sed -i "s/^version:.*/version: '$VERSION'/" snap/snapcraft.yaml
- uses: snapcore/action-build@v1
id: snapcraft
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: snap
path: ${{ steps.snapcraft.outputs.snap }}

View File

@@ -6,14 +6,14 @@ jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: npm run lint
html:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate

View File

@@ -20,8 +20,8 @@ jobs:
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: npm run test
env:

View File

@@ -6,8 +6,8 @@ jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: sudo apt-get install gettext
- run: make -C po update-pot

View File

@@ -32,8 +32,6 @@ for a more complete list with additional info and links.
### News/help/contact
The project website is found at [novnc.com](http://novnc.com).
Notable commits, announcements and news are posted to
[@noVNC](http://www.twitter.com/noVNC).
If you are a noVNC developer/integrator/user (or want to be) please join the
[noVNC discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc).
@@ -59,7 +57,6 @@ profits such as:
[Electronic Frontier Foundation](https://www.eff.org/),
[Against Malaria Foundation](http://www.againstmalaria.com/),
[Nothing But Nets](http://www.nothingbutnets.net/), etc.
Please tweet [@noVNC](http://www.twitter.com/noVNC) if you do.
### Features

View File

@@ -16,13 +16,19 @@ export class Localizer {
this.language = 'en';
// Current dictionary of translations
this.dictionary = undefined;
this._dictionary = undefined;
}
// Configure suitable language based on user preferences
setup(supportedLanguages) {
async setup(supportedLanguages, baseURL) {
this.language = 'en'; // Default: US English
this._dictionary = undefined;
this._setupLanguage(supportedLanguages);
await this._setupDictionary(baseURL);
}
_setupLanguage(supportedLanguages) {
/*
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
* Fall back to navigator.language for other browsers
@@ -40,12 +46,6 @@ export class Localizer {
.replace("_", "-")
.split("-");
// Built-in default?
if ((userLang[0] === 'en') &&
((userLang[1] === undefined) || (userLang[1] === 'us'))) {
return;
}
// First pass: perfect match
for (let j = 0; j < supportedLanguages.length; j++) {
const supLang = supportedLanguages[j]
@@ -64,7 +64,12 @@ export class Localizer {
return;
}
// Second pass: fallback
// Second pass: English fallback
if (userLang[0] === 'en') {
return;
}
// Third pass pass: other fallback
for (let j = 0;j < supportedLanguages.length;j++) {
const supLang = supportedLanguages[j]
.toLowerCase()
@@ -84,10 +89,32 @@ export class Localizer {
}
}
async _setupDictionary(baseURL) {
if (baseURL) {
if (!baseURL.endsWith("/")) {
baseURL = baseURL + "/";
}
} else {
baseURL = "";
}
if (this.language === "en") {
return;
}
let response = await fetch(baseURL + this.language + ".json");
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
this._dictionary = await response.json();
}
// Retrieve localised text
get(id) {
if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) {
return this.dictionary[id];
if (typeof this._dictionary !== 'undefined' &&
this._dictionary[id]) {
return this._dictionary[id];
} else {
return id;
}

View File

@@ -661,7 +661,7 @@ html {
justify-content: center;
align-content: center;
line-height: 25px;
line-height: 1.6;
word-wrap: break-word;
color: #fff;
@@ -887,7 +887,7 @@ html {
.noVNC_logo {
color:yellow;
font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
line-height:90%;
line-height: 0.9;
text-shadow: 0.1em 0.1em 0 black;
}
.noVNC_logo span{

View File

@@ -86,6 +86,9 @@ option {
* Checkboxes
*/
input[type=checkbox] {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: white;
background-image: unset;
border: 1px solid dimgrey;
@@ -104,14 +107,11 @@ input[type=checkbox]:checked {
input[type=checkbox]:checked::after {
content: "";
display: block; /* width & height doesn't work on inline elements */
position: relative;
top: 0;
left: 3px;
width: 3px;
height: 7px;
border: 1px solid white;
border-width: 0 2px 2px 0;
transform: rotate(40deg);
transform: rotate(40deg) translateY(-1px);
}
/*

View File

@@ -66,7 +66,7 @@ const UI = {
// insecure context
if (!window.isSecureContext) {
// FIXME: This gets hidden when connecting
UI.showStatus(_("HTTPS is required for full functionality"), 'error');
UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error');
}
// Try to fetch version number
@@ -1041,10 +1041,18 @@ const UI = {
}
url += '/' + path;
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
{ shared: UI.getSetting('shared'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
try {
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
{ shared: UI.getSetting('shared'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
} catch (exc) {
Log.Error("Failed to connect to server: " + exc);
UI.updateVisualState('disconnected');
UI.showStatus(_("Failed to connect to server: ") + exc, 'error');
return;
}
UI.rfb.addEventListener("connect", UI.connectFinished);
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
UI.rfb.addEventListener("serververification", UI.serverVerify);
@@ -1763,20 +1771,8 @@ const UI = {
// Set up translations
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS);
if (l10n.language === "en" || l10n.dictionary !== undefined) {
UI.prime();
} else {
fetch('app/locale/' + l10n.language + '.json')
.then((response) => {
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
return response.json();
})
.then((translations) => { l10n.dictionary = translations; })
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);
}
l10n.setup(LINGUAS, "app/locale/")
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);
export default UI;

View File

@@ -6,16 +6,16 @@
* See README.md for usage and integration instructions.
*/
import { initLogging as mainInitLogging } from '../core/util/logging.js';
import * as Log from '../core/util/logging.js';
// init log level reading the logging HTTP param
export function initLogging(level) {
"use strict";
if (typeof level !== "undefined") {
mainInitLogging(level);
Log.initLogging(level);
} else {
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
mainInitLogging(param || undefined);
Log.initLogging(param || undefined);
}
}
@@ -25,14 +25,14 @@ export function initLogging(level) {
//
// For privacy (Using a hastag #, the parameters will not be sent to the server)
// the url can be requested in the following way:
// https://www.example.com#myqueryparam=myvalue&password=secreatvalue
// https://www.example.com#myqueryparam=myvalue&password=secretvalue
//
// Even Mixing public and non public parameters will work:
// https://www.example.com?nonsecretparam=example.com#password=secreatvalue
// https://www.example.com?nonsecretparam=example.com#password=secretvalue
export function getQueryVar(name, defVal) {
"use strict";
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = ''.concat(document.location.href, window.location.hash).match(re);
match = document.location.href.match(re);
if (typeof defVal === 'undefined') { defVal = null; }
if (match) {
@@ -146,7 +146,7 @@ export function writeSetting(name, value) {
if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.set(settings);
} else {
localStorage.setItem(name, value);
localStorageSet(name, value);
}
}
@@ -156,7 +156,7 @@ export function readSetting(name, defaultValue) {
if ((name in settings) || (window.chrome && window.chrome.storage)) {
value = settings[name];
} else {
value = localStorage.getItem(name);
value = localStorageGet(name);
settings[name] = value;
}
if (typeof value === "undefined") {
@@ -181,6 +181,70 @@ export function eraseSetting(name) {
if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.remove(name);
} else {
localStorage.removeItem(name);
localStorageRemove(name);
}
}
let loggedMsgs = [];
function logOnce(msg, level = "warn") {
if (!loggedMsgs.includes(msg)) {
switch (level) {
case "error":
Log.Error(msg);
break;
case "warn":
Log.Warn(msg);
break;
case "debug":
Log.Debug(msg);
break;
default:
Log.Info(msg);
}
loggedMsgs.push(msg);
}
}
let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?";
function localStorageGet(name) {
let r;
try {
r = localStorage.getItem(name);
} catch (e) {
if (e instanceof DOMException) {
logOnce(cookiesMsg);
logOnce("'localStorage.getItem(" + name + ")' failed: " + e,
"debug");
} else {
throw e;
}
}
return r;
}
function localStorageSet(name, value) {
try {
localStorage.setItem(name, value);
} catch (e) {
if (e instanceof DOMException) {
logOnce(cookiesMsg);
logOnce("'localStorage.setItem(" + name + "," + value +
")' failed: " + e, "debug");
} else {
throw e;
}
}
}
function localStorageRemove(name) {
try {
localStorage.removeItem(name);
} catch (e) {
if (e instanceof DOMException) {
logOnce(cookiesMsg);
logOnce("'localStorage.removeItem(" + name + ")' failed: " + e,
"debug");
} else {
throw e;
}
}
}

178
core/crypto/aes.js Normal file
View File

@@ -0,0 +1,178 @@
export class AESECBCipher {
constructor() {
this._key = null;
}
get algorithm() {
return { name: "AES-ECB" };
}
static async importKey(key, _algorithm, extractable, keyUsages) {
const cipher = new AESECBCipher;
await cipher._importKey(key, extractable, keyUsages);
return cipher;
}
async _importKey(key, extractable, keyUsages) {
this._key = await window.crypto.subtle.importKey(
"raw", key, {name: "AES-CBC"}, extractable, keyUsages);
}
async encrypt(_algorithm, plaintext) {
const x = new Uint8Array(plaintext);
if (x.length % 16 !== 0 || this._key === null) {
return null;
}
const n = x.length / 16;
for (let i = 0; i < n; i++) {
const y = new Uint8Array(await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: new Uint8Array(16),
}, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);
x.set(y, i * 16);
}
return x;
}
}
export class AESEAXCipher {
constructor() {
this._rawKey = null;
this._ctrKey = null;
this._cbcKey = null;
this._zeroBlock = new Uint8Array(16);
this._prefixBlock0 = this._zeroBlock;
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
}
get algorithm() {
return { name: "AES-EAX" };
}
async _encryptBlock(block) {
const encrypted = await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: this._zeroBlock,
}, this._cbcKey, block);
return new Uint8Array(encrypted).slice(0, 16);
}
async _initCMAC() {
const k1 = await this._encryptBlock(this._zeroBlock);
const k2 = new Uint8Array(16);
const v = k1[0] >>> 6;
for (let i = 0; i < 15; i++) {
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
}
const lut = [0x0, 0x87, 0x0e, 0x89];
k2[14] ^= v >>> 1;
k2[15] = (k1[15] << 2) ^ lut[v];
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
this._k1 = k1;
this._k2 = k2;
}
async _encryptCTR(data, counter) {
const encrypted = await window.crypto.subtle.encrypt({
name: "AES-CTR",
counter: counter,
length: 128
}, this._ctrKey, data);
return new Uint8Array(encrypted);
}
async _decryptCTR(data, counter) {
const decrypted = await window.crypto.subtle.decrypt({
name: "AES-CTR",
counter: counter,
length: 128
}, this._ctrKey, data);
return new Uint8Array(decrypted);
}
async _computeCMAC(data, prefixBlock) {
if (prefixBlock.length !== 16) {
return null;
}
const n = Math.floor(data.length / 16);
const m = Math.ceil(data.length / 16);
const r = data.length - n * 16;
const cbcData = new Uint8Array((m + 1) * 16);
cbcData.set(prefixBlock);
cbcData.set(data, 16);
if (r === 0) {
for (let i = 0; i < 16; i++) {
cbcData[n * 16 + i] ^= this._k1[i];
}
} else {
cbcData[(n + 1) * 16 + r] = 0x80;
for (let i = 0; i < 16; i++) {
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
}
}
let cbcEncrypted = await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: this._zeroBlock,
}, this._cbcKey, cbcData);
cbcEncrypted = new Uint8Array(cbcEncrypted);
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
return mac;
}
static async importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new AESEAXCipher;
await cipher._importKey(key);
return cipher;
}
async _importKey(key) {
this._rawKey = key;
this._ctrKey = await window.crypto.subtle.importKey(
"raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
this._cbcKey = await window.crypto.subtle.importKey(
"raw", key, {name: "AES-CBC"}, false, ["encrypt"]);
await this._initCMAC();
}
async encrypt(algorithm, message) {
const ad = algorithm.additionalData;
const nonce = algorithm.iv;
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const encrypted = await this._encryptCTR(message, nCMAC);
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
for (let i = 0; i < 16; i++) {
mac[i] ^= nCMAC[i] ^ adCMAC[i];
}
const res = new Uint8Array(16 + encrypted.length);
res.set(encrypted);
res.set(mac, encrypted.length);
return res;
}
async decrypt(algorithm, data) {
const encrypted = data.slice(0, data.length - 16);
const ad = algorithm.additionalData;
const nonce = algorithm.iv;
const mac = data.slice(data.length - 16);
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
for (let i = 0; i < 16; i++) {
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
}
if (computedMac.length !== mac.length) {
return null;
}
for (let i = 0; i < mac.length; i++) {
if (computedMac[i] !== mac[i]) {
return null;
}
}
const res = await this._decryptCTR(encrypted, nCMAC);
return res;
}
}

34
core/crypto/bigint.js Normal file
View File

@@ -0,0 +1,34 @@
export function modPow(b, e, m) {
let r = 1n;
b = b % m;
while (e > 0n) {
if ((e & 1n) === 1n) {
r = (r * b) % m;
}
e = e >> 1n;
b = (b * b) % m;
}
return r;
}
export function bigIntToU8Array(bigint, padLength=0) {
let hex = bigint.toString(16);
if (padLength === 0) {
padLength = Math.ceil(hex.length / 2);
}
hex = hex.padStart(padLength * 2, '0');
const length = hex.length / 2;
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return arr;
}
export function u8ArrayToBigInt(arr) {
let hex = '0x';
for (let i = 0; i < arr.length; i++) {
hex += arr[i].toString(16).padStart(2, '0');
}
return BigInt(hex);
}

90
core/crypto/crypto.js Normal file
View File

@@ -0,0 +1,90 @@
import { AESECBCipher, AESEAXCipher } from "./aes.js";
import { DESCBCCipher, DESECBCipher } from "./des.js";
import { RSACipher } from "./rsa.js";
import { DHCipher } from "./dh.js";
import { MD5 } from "./md5.js";
// A single interface for the cryptographic algorithms not supported by SubtleCrypto.
// Both synchronous and asynchronous implmentations are allowed.
class LegacyCrypto {
constructor() {
this._algorithms = {
"AES-ECB": AESECBCipher,
"AES-EAX": AESEAXCipher,
"DES-ECB": DESECBCipher,
"DES-CBC": DESCBCCipher,
"RSA-PKCS1-v1_5": RSACipher,
"DH": DHCipher,
"MD5": MD5,
};
}
encrypt(algorithm, key, data) {
if (key.algorithm.name !== algorithm.name) {
throw new Error("algorithm does not match");
}
if (typeof key.encrypt !== "function") {
throw new Error("key does not support encryption");
}
return key.encrypt(algorithm, data);
}
decrypt(algorithm, key, data) {
if (key.algorithm.name !== algorithm.name) {
throw new Error("algorithm does not match");
}
if (typeof key.decrypt !== "function") {
throw new Error("key does not support encryption");
}
return key.decrypt(algorithm, data);
}
importKey(format, keyData, algorithm, extractable, keyUsages) {
if (format !== "raw") {
throw new Error("key format is not supported");
}
const alg = this._algorithms[algorithm.name];
if (typeof alg === "undefined" || typeof alg.importKey !== "function") {
throw new Error("algorithm is not supported");
}
return alg.importKey(keyData, algorithm, extractable, keyUsages);
}
generateKey(algorithm, extractable, keyUsages) {
const alg = this._algorithms[algorithm.name];
if (typeof alg === "undefined" || typeof alg.generateKey !== "function") {
throw new Error("algorithm is not supported");
}
return alg.generateKey(algorithm, extractable, keyUsages);
}
exportKey(format, key) {
if (format !== "raw") {
throw new Error("key format is not supported");
}
if (typeof key.exportKey !== "function") {
throw new Error("key does not support exportKey");
}
return key.exportKey();
}
digest(algorithm, data) {
const alg = this._algorithms[algorithm];
if (typeof alg !== "function") {
throw new Error("algorithm is not supported");
}
return alg(data);
}
deriveBits(algorithm, key, length) {
if (key.algorithm.name !== algorithm.name) {
throw new Error("algorithm does not match");
}
if (typeof key.deriveBits !== "function") {
throw new Error("key does not support deriveBits");
}
return key.deriveBits(algorithm, length);
}
}
export default new LegacyCrypto;

View File

@@ -128,7 +128,7 @@ const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
/* eslint-enable comma-spacing */
export default class DES {
class DES {
constructor(password) {
this.keys = [];
@@ -258,9 +258,73 @@ export default class DES {
}
return b;
}
}
// Encrypt 16 bytes of text using passwd as key
encrypt(t) {
return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16)));
export class DESECBCipher {
constructor() {
this._cipher = null;
}
get algorithm() {
return { name: "DES-ECB" };
}
static importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new DESECBCipher;
cipher._importKey(key);
return cipher;
}
_importKey(key, _extractable, _keyUsages) {
this._cipher = new DES(key);
}
encrypt(_algorithm, plaintext) {
const x = new Uint8Array(plaintext);
if (x.length % 8 !== 0 || this._cipher === null) {
return null;
}
const n = x.length / 8;
for (let i = 0; i < n; i++) {
x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
}
return x;
}
}
export class DESCBCCipher {
constructor() {
this._cipher = null;
}
get algorithm() {
return { name: "DES-CBC" };
}
static importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new DESCBCCipher;
cipher._importKey(key);
return cipher;
}
_importKey(key) {
this._cipher = new DES(key);
}
encrypt(algorithm, plaintext) {
const x = new Uint8Array(plaintext);
let y = new Uint8Array(algorithm.iv);
if (x.length % 8 !== 0 || this._cipher === null) {
return null;
}
const n = x.length / 8;
for (let i = 0; i < n; i++) {
for (let j = 0; j < 8; j++) {
y[j] ^= plaintext[i * 8 + j];
}
y = this._cipher.enc8(y);
x.set(y, i * 8);
}
return x;
}
}

55
core/crypto/dh.js Normal file
View File

@@ -0,0 +1,55 @@
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
class DHPublicKey {
constructor(key) {
this._key = key;
}
get algorithm() {
return { name: "DH" };
}
exportKey() {
return this._key;
}
}
export class DHCipher {
constructor() {
this._g = null;
this._p = null;
this._gBigInt = null;
this._pBigInt = null;
this._privateKey = null;
}
get algorithm() {
return { name: "DH" };
}
static generateKey(algorithm, _extractable) {
const cipher = new DHCipher;
cipher._generateKey(algorithm);
return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };
}
_generateKey(algorithm) {
const g = algorithm.g;
const p = algorithm.p;
this._keyBytes = p.length;
this._gBigInt = u8ArrayToBigInt(g);
this._pBigInt = u8ArrayToBigInt(p);
this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));
this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);
this._publicKey = bigIntToU8Array(modPow(
this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);
}
deriveBits(algorithm, length) {
const bytes = Math.ceil(length / 8);
const pkey = new Uint8Array(algorithm.public);
const len = bytes > this._keyBytes ? bytes : this._keyBytes;
const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);
return bigIntToU8Array(secret, len).slice(0, len);
}
}

View File

@@ -7,12 +7,15 @@
*/
/*
* Performs MD5 hashing on a string of binary characters, returns an array of bytes
* Performs MD5 hashing on an array of bytes, returns an array of bytes
*/
export function MD5(d) {
let r = M(V(Y(X(d), 8 * d.length)));
return r;
export async function MD5(d) {
let s = "";
for (let i = 0; i < d.length; i++) {
s += String.fromCharCode(d[i]);
}
return M(V(Y(X(s), 8 * s.length)));
}
function M(d) {
@@ -76,4 +79,4 @@ function add(d, g) {
function rol(d, g) {
return d << g | d >>> 32 - g;
}
}

132
core/crypto/rsa.js Normal file
View File

@@ -0,0 +1,132 @@
import Base64 from "../base64.js";
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
export class RSACipher {
constructor() {
this._keyLength = 0;
this._keyBytes = 0;
this._n = null;
this._e = null;
this._d = null;
this._nBigInt = null;
this._eBigInt = null;
this._dBigInt = null;
this._extractable = false;
}
get algorithm() {
return { name: "RSA-PKCS1-v1_5" };
}
_base64urlDecode(data) {
data = data.replace(/-/g, "+").replace(/_/g, "/");
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
return Base64.decode(data);
}
_padArray(arr, length) {
const res = new Uint8Array(length);
res.set(arr, length - arr.length);
return res;
}
static async generateKey(algorithm, extractable, _keyUsages) {
const cipher = new RSACipher;
await cipher._generateKey(algorithm, extractable);
return { privateKey: cipher };
}
async _generateKey(algorithm, extractable) {
this._keyLength = algorithm.modulusLength;
this._keyBytes = Math.ceil(this._keyLength / 8);
const key = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: algorithm.modulusLength,
publicExponent: algorithm.publicExponent,
hash: {name: "SHA-256"},
},
true, ["encrypt", "decrypt"]);
const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey);
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
this._nBigInt = u8ArrayToBigInt(this._n);
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
this._eBigInt = u8ArrayToBigInt(this._e);
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
this._dBigInt = u8ArrayToBigInt(this._d);
this._extractable = extractable;
}
static async importKey(key, _algorithm, extractable, keyUsages) {
if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") {
throw new Error("only support importing RSA public key");
}
const cipher = new RSACipher;
await cipher._importKey(key, extractable);
return cipher;
}
async _importKey(key, extractable) {
const n = key.n;
const e = key.e;
if (n.length !== e.length) {
throw new Error("the sizes of modulus and public exponent do not match");
}
this._keyBytes = n.length;
this._keyLength = this._keyBytes * 8;
this._n = new Uint8Array(this._keyBytes);
this._e = new Uint8Array(this._keyBytes);
this._n.set(n);
this._e.set(e);
this._nBigInt = u8ArrayToBigInt(this._n);
this._eBigInt = u8ArrayToBigInt(this._e);
this._extractable = extractable;
}
async encrypt(_algorithm, message) {
if (message.length > this._keyBytes - 11) {
return null;
}
const ps = new Uint8Array(this._keyBytes - message.length - 3);
window.crypto.getRandomValues(ps);
for (let i = 0; i < ps.length; i++) {
ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
}
const em = new Uint8Array(this._keyBytes);
em[1] = 0x02;
em.set(ps, 2);
em.set(message, ps.length + 3);
const emBigInt = u8ArrayToBigInt(em);
const c = modPow(emBigInt, this._eBigInt, this._nBigInt);
return bigIntToU8Array(c, this._keyBytes);
}
async decrypt(_algorithm, message) {
if (message.length !== this._keyBytes) {
return null;
}
const msgBigInt = u8ArrayToBigInt(message);
const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);
const em = bigIntToU8Array(emBigInt, this._keyBytes);
if (em[0] !== 0x00 || em[1] !== 0x02) {
return null;
}
let i = 2;
for (; i < em.length; i++) {
if (em[i] === 0x00) {
break;
}
}
if (i === em.length) {
return null;
}
return em.slice(i + 1, em.length);
}
async exportKey() {
if (!this._extractable) {
throw new Error("key is not extractable");
}
return { n: this._n, e: this._e, d: this._d };
}
}

View File

@@ -31,10 +31,7 @@ export default class HextileDecoder {
return false;
}
let rQ = sock.rQ;
let rQi = sock.rQi;
let subencoding = rQ[rQi]; // Peek
let subencoding = sock.rQpeek8();
if (subencoding > 30) { // Raw
throw new Error("Illegal hextile subencoding (subencoding: " +
subencoding + ")");
@@ -65,7 +62,7 @@ export default class HextileDecoder {
return false;
}
let subrects = rQ[rQi + bytes - 1]; // Peek
let subrects = sock.rQpeekBytes(bytes).at(-1);
if (subencoding & 0x10) { // SubrectsColoured
bytes += subrects * (4 + 2);
} else {
@@ -79,7 +76,7 @@ export default class HextileDecoder {
}
// We know the encoding and have a whole tile
rQi++;
sock.rQshift8();
if (subencoding === 0) {
if (this._lastsubencoding & 0x01) {
// Weird: ignore blanks are RAW
@@ -89,42 +86,36 @@ export default class HextileDecoder {
}
} else if (subencoding & 0x01) { // Raw
let pixels = tw * th;
let data = sock.rQshiftBytes(pixels * 4, false);
// Max sure the image is fully opaque
for (let i = 0;i < pixels;i++) {
rQ[rQi + i * 4 + 3] = 255;
data[i * 4 + 3] = 255;
}
display.blitImage(tx, ty, tw, th, rQ, rQi);
rQi += bytes - 1;
display.blitImage(tx, ty, tw, th, data, 0);
} else {
if (subencoding & 0x02) { // Background
this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
rQi += 4;
this._background = new Uint8Array(sock.rQshiftBytes(4));
}
if (subencoding & 0x04) { // Foreground
this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
rQi += 4;
this._foreground = new Uint8Array(sock.rQshiftBytes(4));
}
this._startTile(tx, ty, tw, th, this._background);
if (subencoding & 0x08) { // AnySubrects
let subrects = rQ[rQi];
rQi++;
let subrects = sock.rQshift8();
for (let s = 0; s < subrects; s++) {
let color;
if (subencoding & 0x10) { // SubrectsColoured
color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
rQi += 4;
color = sock.rQshiftBytes(4);
} else {
color = this._foreground;
}
const xy = rQ[rQi];
rQi++;
const xy = sock.rQshift8();
const sx = (xy >> 4);
const sy = (xy & 0x0f);
const wh = rQ[rQi];
rQi++;
const wh = sock.rQshift8();
const sw = (wh >> 4) + 1;
const sh = (wh & 0x0f) + 1;
@@ -133,7 +124,6 @@ export default class HextileDecoder {
}
this._finishTile(display);
}
sock.rQi = rQi;
this._lastsubencoding = subencoding;
this._tiles--;
}

View File

@@ -11,131 +11,136 @@ export default class JPEGDecoder {
constructor() {
// RealVNC will reuse the quantization tables
// and Huffman tables, so we need to cache them.
this._quantTables = [];
this._huffmanTables = [];
this._cachedQuantTables = [];
this._cachedHuffmanTables = [];
this._jpegLength = 0;
this._segments = [];
}
decodeRect(x, y, width, height, sock, display, depth) {
// A rect of JPEG encodings is simply a JPEG file
if (!this._parseJPEG(sock.rQslice(0))) {
return false;
}
const data = sock.rQshiftBytes(this._jpegLength);
if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
// If there are quantization tables and Huffman tables in the JPEG
// image, we can directly render it.
display.imageRect(x, y, width, height, "image/jpeg", data);
return true;
} else {
// Otherwise we need to insert cached tables.
const sofIndex = this._segments.findIndex(
x => x[1] == 0xC0 || x[1] == 0xC2
);
if (sofIndex == -1) {
throw new Error("Illegal JPEG image without SOF");
}
let segments = this._segments.slice(0, sofIndex);
segments = segments.concat(this._quantTables.length ?
this._quantTables :
this._cachedQuantTables);
segments.push(this._segments[sofIndex]);
segments = segments.concat(this._huffmanTables.length ?
this._huffmanTables :
this._cachedHuffmanTables,
this._segments.slice(sofIndex + 1));
let length = 0;
for (let i = 0; i < segments.length; i++) {
length += segments[i].length;
}
const data = new Uint8Array(length);
length = 0;
for (let i = 0; i < segments.length; i++) {
data.set(segments[i], length);
length += segments[i].length;
}
display.imageRect(x, y, width, height, "image/jpeg", data);
return true;
}
}
_parseJPEG(buffer) {
if (this._quantTables.length != 0) {
this._cachedQuantTables = this._quantTables;
}
if (this._huffmanTables.length != 0) {
this._cachedHuffmanTables = this._huffmanTables;
}
this._quantTables = [];
this._huffmanTables = [];
this._segments = [];
let i = 0;
let bufferLength = buffer.length;
while (true) {
let j = i;
if (j + 2 > bufferLength) {
let segment = this._readSegment(sock);
if (segment === null) {
return false;
}
if (buffer[j] != 0xFF) {
throw new Error("Illegal JPEG marker received (byte: " +
buffer[j] + ")");
}
const type = buffer[j+1];
j += 2;
if (type == 0xD9) {
this._jpegLength = j;
this._segments.push(buffer.slice(i, j));
return true;
} else if (type == 0xDA) {
// start of scan
let hasFoundEndOfScan = false;
for (let k = j + 3; k + 1 < bufferLength; k++) {
if (buffer[k] == 0xFF && buffer[k+1] != 0x00 &&
!(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) {
j = k;
hasFoundEndOfScan = true;
break;
}
}
if (!hasFoundEndOfScan) {
return false;
}
this._segments.push(buffer.slice(i, j));
i = j;
continue;
} else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
// No length after marker
this._segments.push(buffer.slice(i, j));
i = j;
continue;
}
if (j + 2 > bufferLength) {
return false;
}
const length = (buffer[j] << 8) + buffer[j+1] - 2;
if (length < 0) {
throw new Error("Illegal JPEG length received (length: " +
length + ")");
}
j += 2;
if (j + length > bufferLength) {
return false;
}
j += length;
const segment = buffer.slice(i, j);
if (type == 0xC4) {
// Huffman tables
this._huffmanTables.push(segment);
} else if (type == 0xDB) {
// Quantization tables
this._quantTables.push(segment);
}
this._segments.push(segment);
i = j;
// End of image?
if (segment[1] === 0xD9) {
break;
}
}
let huffmanTables = [];
let quantTables = [];
for (let segment of this._segments) {
let type = segment[1];
if (type === 0xC4) {
// Huffman tables
huffmanTables.push(segment);
} else if (type === 0xDB) {
// Quantization tables
quantTables.push(segment);
}
}
const sofIndex = this._segments.findIndex(
x => x[1] == 0xC0 || x[1] == 0xC2
);
if (sofIndex == -1) {
throw new Error("Illegal JPEG image without SOF");
}
if (quantTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedQuantTables);
}
if (huffmanTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedHuffmanTables);
}
let length = 0;
for (let segment of this._segments) {
length += segment.length;
}
let data = new Uint8Array(length);
length = 0;
for (let segment of this._segments) {
data.set(segment, length);
length += segment.length;
}
display.imageRect(x, y, width, height, "image/jpeg", data);
if (huffmanTables.length !== 0) {
this._cachedHuffmanTables = huffmanTables;
}
if (quantTables.length !== 0) {
this._cachedQuantTables = quantTables;
}
this._segments = [];
return true;
}
_readSegment(sock) {
if (sock.rQwait("JPEG", 2)) {
return null;
}
let marker = sock.rQshift8();
if (marker != 0xFF) {
throw new Error("Illegal JPEG marker received (byte: " +
marker + ")");
}
let type = sock.rQshift8();
if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
// No length after marker
return new Uint8Array([marker, type]);
}
if (sock.rQwait("JPEG", 2, 2)) {
return null;
}
let length = sock.rQshift16();
if (length < 2) {
throw new Error("Illegal JPEG length received (length: " +
length + ")");
}
if (sock.rQwait("JPEG", length-2, 4)) {
return null;
}
let extra = 0;
if (type === 0xDA) {
// start of scan
extra += 2;
while (true) {
if (sock.rQwait("JPEG", length-2+extra, 4)) {
return null;
}
let data = sock.rQpeekBytes(length-2+extra, false);
if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
extra -= 2;
break;
}
extra++;
}
}
let segment = new Uint8Array(2 + length + extra);
segment[0] = marker;
segment[1] = type;
segment[2] = length >> 8;
segment[3] = length;
segment.set(sock.rQshiftBytes(length-2+extra, false), 4);
return segment;
}
}

View File

@@ -24,41 +24,34 @@ export default class RawDecoder {
const pixelSize = depth == 8 ? 1 : 4;
const bytesPerLine = width * pixelSize;
if (sock.rQwait("RAW", bytesPerLine)) {
return false;
}
const curY = y + (height - this._lines);
const currHeight = Math.min(this._lines,
Math.floor(sock.rQlen / bytesPerLine));
const pixels = width * currHeight;
let data = sock.rQ;
let index = sock.rQi;
// Convert data if needed
if (depth == 8) {
const newdata = new Uint8Array(pixels * 4);
for (let i = 0; i < pixels; i++) {
newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3;
newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3;
newdata[i * 4 + 3] = 255;
while (this._lines > 0) {
if (sock.rQwait("RAW", bytesPerLine)) {
return false;
}
data = newdata;
index = 0;
}
// Max sure the image is fully opaque
for (let i = 0; i < pixels; i++) {
data[index + i * 4 + 3] = 255;
}
const curY = y + (height - this._lines);
display.blitImage(x, curY, width, currHeight, data, index);
sock.rQskipBytes(currHeight * bytesPerLine);
this._lines -= currHeight;
if (this._lines > 0) {
return false;
let data = sock.rQshiftBytes(bytesPerLine, false);
// Convert data if needed
if (depth == 8) {
const newdata = new Uint8Array(width * 4);
for (let i = 0; i < width; i++) {
newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;
newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;
newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;
newdata[i * 4 + 3] = 255;
}
data = newdata;
}
// Max sure the image is fully opaque
for (let i = 0; i < width; i++) {
data[i * 4 + 3] = 255;
}
display.blitImage(x, curY, width, 1, data, 0);
this._lines--;
}
return true;

View File

@@ -76,12 +76,8 @@ export default class TightDecoder {
return false;
}
const rQi = sock.rQi;
const rQ = sock.rQ;
display.fillRect(x, y, width, height,
[rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
sock.rQskipBytes(3);
let pixel = sock.rQshiftBytes(3);
display.fillRect(x, y, width, height, pixel, false);
return true;
}
@@ -289,7 +285,73 @@ export default class TightDecoder {
}
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
throw new Error("Gradient filter not implemented");
// assume the TPIXEL is 3 bytes long
const uncompressedSize = width * height * 3;
let data;
if (uncompressedSize === 0) {
return true;
}
if (uncompressedSize < 12) {
if (sock.rQwait("TIGHT", uncompressedSize)) {
return false;
}
data = sock.rQshiftBytes(uncompressedSize);
} else {
data = this._readData(sock);
if (data === null) {
return false;
}
this._zlibs[streamId].setInput(data);
data = this._zlibs[streamId].inflate(uncompressedSize);
this._zlibs[streamId].setInput(null);
}
let rgbx = new Uint8Array(4 * width * height);
let rgbxIndex = 0, dataIndex = 0;
let left = new Uint8Array(3);
for (let x = 0; x < width; x++) {
for (let c = 0; c < 3; c++) {
const prediction = left[c];
const value = data[dataIndex++] + prediction;
rgbx[rgbxIndex++] = value;
left[c] = value;
}
rgbx[rgbxIndex++] = 255;
}
let upperIndex = 0;
let upper = new Uint8Array(3),
upperleft = new Uint8Array(3);
for (let y = 1; y < height; y++) {
left.fill(0);
upperleft.fill(0);
for (let x = 0; x < width; x++) {
for (let c = 0; c < 3; c++) {
upper[c] = rgbx[upperIndex++];
let prediction = left[c] + upper[c] - upperleft[c];
if (prediction < 0) {
prediction = 0;
} else if (prediction > 255) {
prediction = 255;
}
const value = data[dataIndex++] + prediction;
rgbx[rgbxIndex++] = value;
upperleft[c] = upper[c];
left[c] = value;
}
rgbx[rgbxIndex++] = 255;
upperIndex++;
}
}
display.blitImage(x, y, width, height, rgbx, 0, false);
return true;
}
_readData(sock) {
@@ -316,7 +378,7 @@ export default class TightDecoder {
return null;
}
let data = sock.rQshiftBytes(this._len);
let data = sock.rQshiftBytes(this._len, false);
this._len = 0;
return data;

View File

@@ -32,7 +32,7 @@ export default class ZRLEDecoder {
return false;
}
const data = sock.rQshiftBytes(this._length);
const data = sock.rQshiftBytes(this._length, false);
this._inflator.setInput(data);

View File

@@ -7,7 +7,7 @@
*/
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
export default class Deflator {
@@ -15,9 +15,8 @@ export default class Deflator {
this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10;
this.outputBuffer = new Uint8Array(this.chunkSize);
this.windowBits = 5;
deflateInit(this.strm, this.windowBits);
deflateInit(this.strm, Z_DEFAULT_COMPRESSION);
}
deflate(inData) {

View File

@@ -15,7 +15,7 @@ export default class Display {
this._drawCtx = null;
this._renderQ = []; // queue drawing actions for in-oder rendering
this._flushing = false;
this._flushPromise = null;
// the full frame buffer (logical canvas) size
this._fbWidth = 0;
@@ -61,10 +61,6 @@ export default class Display {
this._scale = 1.0;
this._clipViewport = false;
// ===== EVENT HANDLERS =====
this.onflush = () => {}; // A flush request has finished
}
// ===== PROPERTIES =====
@@ -306,9 +302,14 @@ export default class Display {
flush() {
if (this._renderQ.length === 0) {
this.onflush();
return Promise.resolve();
} else {
this._flushing = true;
if (this._flushPromise === null) {
this._flushPromise = new Promise((resolve) => {
this._flushResolve = resolve;
});
}
return this._flushPromise;
}
}
@@ -517,9 +518,11 @@ export default class Display {
}
}
if (this._renderQ.length === 0 && this._flushing) {
this._flushing = false;
this.onflush();
if (this._renderQ.length === 0 &&
this._flushPromise !== null) {
this._flushResolve();
this._flushPromise = null;
this._flushResolve = null;
}
}
}

View File

@@ -22,6 +22,7 @@ export const encodings = {
pseudoEncodingLastRect: -224,
pseudoEncodingCursor: -239,
pseudoEncodingQEMUExtendedKeyEvent: -258,
pseudoEncodingQEMULedEvent: -261,
pseudoEncodingDesktopName: -307,
pseudoEncodingExtendedDesktopSize: -308,
pseudoEncodingXvp: -309,

View File

@@ -14,9 +14,8 @@ export default class Inflate {
this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10;
this.strm.output = new Uint8Array(this.chunkSize);
this.windowBits = 5;
inflateInit(this.strm, this.windowBits);
inflateInit(this.strm);
}
setInput(data) {

View File

@@ -36,7 +36,7 @@ export default class Keyboard {
// ===== PRIVATE METHODS =====
_sendKeyEvent(keysym, code, down) {
_sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {
if (down) {
this._keyDownList[code] = keysym;
} else {
@@ -48,8 +48,9 @@ export default class Keyboard {
}
Log.Debug("onkeyevent " + (down ? "down" : "up") +
", keysym: " + keysym, ", code: " + code);
this.onkeyevent(keysym, code, down);
", keysym: " + keysym, ", code: " + code +
", numlock: " + numlock + ", capslock: " + capslock);
this.onkeyevent(keysym, code, down, numlock, capslock);
}
_getKeyCode(e) {
@@ -86,6 +87,14 @@ export default class Keyboard {
_handleKeyDown(e) {
const code = this._getKeyCode(e);
let keysym = KeyboardUtil.getKeysym(e);
let numlock = e.getModifierState('NumLock');
let capslock = e.getModifierState('CapsLock');
// getModifierState for NumLock is not supported on mac and ios and always returns false.
// Set to null to indicate unknown/unsupported instead.
if (browser.isMac() || browser.isIOS()) {
numlock = null;
}
// Windows doesn't have a proper AltGr, but handles it using
// fake Ctrl+Alt. However the remote end might not be Windows,
@@ -107,7 +116,7 @@ export default class Keyboard {
// key to "AltGraph".
keysym = KeyTable.XK_ISO_Level3_Shift;
} else {
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock);
}
}
@@ -118,8 +127,8 @@ export default class Keyboard {
// If it's a virtual keyboard then it should be
// sufficient to just send press and release right
// after each other
this._sendKeyEvent(keysym, code, true);
this._sendKeyEvent(keysym, code, false);
this._sendKeyEvent(keysym, code, true, numlock, capslock);
this._sendKeyEvent(keysym, code, false, numlock, capslock);
}
stopEvent(e);
@@ -157,8 +166,8 @@ export default class Keyboard {
// while meta is held down
if ((browser.isMac() || browser.isIOS()) &&
(e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
this._sendKeyEvent(keysym, code, true);
this._sendKeyEvent(keysym, code, false);
this._sendKeyEvent(keysym, code, true, numlock, capslock);
this._sendKeyEvent(keysym, code, false, numlock, capslock);
stopEvent(e);
return;
}
@@ -168,8 +177,8 @@ export default class Keyboard {
// which toggles on each press, but not on release. So pretend
// it was a quick press and release of the button.
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock);
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock);
stopEvent(e);
return;
}
@@ -182,8 +191,8 @@ export default class Keyboard {
KeyTable.XK_Hiragana,
KeyTable.XK_Romaji ];
if (browser.isWindows() && jpBadKeys.includes(keysym)) {
this._sendKeyEvent(keysym, code, true);
this._sendKeyEvent(keysym, code, false);
this._sendKeyEvent(keysym, code, true, numlock, capslock);
this._sendKeyEvent(keysym, code, false, numlock, capslock);
stopEvent(e);
return;
}
@@ -199,7 +208,7 @@ export default class Keyboard {
return;
}
this._sendKeyEvent(keysym, code, true);
this._sendKeyEvent(keysym, code, true, numlock, capslock);
}
_handleKeyUp(e) {

View File

@@ -67,7 +67,7 @@ export function getKeycode(evt) {
// Get 'KeyboardEvent.key', handling legacy browsers
export function getKey(evt) {
// Are we getting a proper key value?
if (evt.key !== undefined) {
if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) {
// Mozilla isn't fully in sync with the spec yet
switch (evt.key) {
case 'OS': return 'Meta';

View File

@@ -1,146 +1,25 @@
import Base64 from './base64.js';
import { encodeUTF8 } from './util/strings.js';
import EventTargetMixin from './util/eventtarget.js';
import legacyCrypto from './crypto/crypto.js';
export class AESEAXCipher {
class RA2Cipher {
constructor() {
this._rawKey = null;
this._ctrKey = null;
this._cbcKey = null;
this._zeroBlock = new Uint8Array(16);
this._prefixBlock0 = this._zeroBlock;
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
}
async _encryptBlock(block) {
const encrypted = await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: this._zeroBlock,
}, this._cbcKey, block);
return new Uint8Array(encrypted).slice(0, 16);
}
async _initCMAC() {
const k1 = await this._encryptBlock(this._zeroBlock);
const k2 = new Uint8Array(16);
const v = k1[0] >>> 6;
for (let i = 0; i < 15; i++) {
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
}
const lut = [0x0, 0x87, 0x0e, 0x89];
k2[14] ^= v >>> 1;
k2[15] = (k1[15] << 2) ^ lut[v];
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
this._k1 = k1;
this._k2 = k2;
}
async _encryptCTR(data, counter) {
const encrypted = await window.crypto.subtle.encrypt({
"name": "AES-CTR",
counter: counter,
length: 128
}, this._ctrKey, data);
return new Uint8Array(encrypted);
}
async _decryptCTR(data, counter) {
const decrypted = await window.crypto.subtle.decrypt({
"name": "AES-CTR",
counter: counter,
length: 128
}, this._ctrKey, data);
return new Uint8Array(decrypted);
}
async _computeCMAC(data, prefixBlock) {
if (prefixBlock.length !== 16) {
return null;
}
const n = Math.floor(data.length / 16);
const m = Math.ceil(data.length / 16);
const r = data.length - n * 16;
const cbcData = new Uint8Array((m + 1) * 16);
cbcData.set(prefixBlock);
cbcData.set(data, 16);
if (r === 0) {
for (let i = 0; i < 16; i++) {
cbcData[n * 16 + i] ^= this._k1[i];
}
} else {
cbcData[(n + 1) * 16 + r] = 0x80;
for (let i = 0; i < 16; i++) {
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
}
}
let cbcEncrypted = await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: this._zeroBlock,
}, this._cbcKey, cbcData);
cbcEncrypted = new Uint8Array(cbcEncrypted);
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
return mac;
}
async setKey(key) {
this._rawKey = key;
this._ctrKey = await window.crypto.subtle.importKey(
"raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]);
this._cbcKey = await window.crypto.subtle.importKey(
"raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]);
await this._initCMAC();
}
async encrypt(message, associatedData, nonce) {
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const encrypted = await this._encryptCTR(message, nCMAC);
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
for (let i = 0; i < 16; i++) {
mac[i] ^= nCMAC[i] ^ adCMAC[i];
}
const res = new Uint8Array(16 + encrypted.length);
res.set(encrypted);
res.set(mac, encrypted.length);
return res;
}
async decrypt(encrypted, associatedData, nonce, mac) {
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
for (let i = 0; i < 16; i++) {
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
}
if (computedMac.length !== mac.length) {
return null;
}
for (let i = 0; i < mac.length; i++) {
if (computedMac[i] !== mac[i]) {
return null;
}
}
const res = await this._decryptCTR(encrypted, nCMAC);
return res;
}
}
export class RA2Cipher {
constructor() {
this._cipher = new AESEAXCipher();
this._cipher = null;
this._counter = new Uint8Array(16);
}
async setKey(key) {
await this._cipher.setKey(key);
this._cipher = await legacyCrypto.importKey(
"raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
}
async makeMessage(message) {
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
const encrypted = await this._cipher.encrypt(message, ad, this._counter);
const encrypted = await legacyCrypto.encrypt({
name: "AES-EAX",
iv: this._counter,
additionalData: ad,
}, this._cipher, message);
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
const res = new Uint8Array(message.length + 2 + 16);
res.set(ad);
@@ -148,164 +27,18 @@ export class RA2Cipher {
return res;
}
async receiveMessage(length, encrypted, mac) {
async receiveMessage(length, encrypted) {
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac);
const res = await legacyCrypto.decrypt({
name: "AES-EAX",
iv: this._counter,
additionalData: ad,
}, this._cipher, encrypted);
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
return res;
}
}
export class RSACipher {
constructor(keyLength) {
this._key = null;
this._keyLength = keyLength;
this._keyBytes = Math.ceil(keyLength / 8);
this._n = null;
this._e = null;
this._d = null;
this._nBigInt = null;
this._eBigInt = null;
this._dBigInt = null;
}
_base64urlDecode(data) {
data = data.replace(/-/g, "+").replace(/_/g, "/");
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
return Base64.decode(data);
}
_u8ArrayToBigInt(arr) {
let hex = '0x';
for (let i = 0; i < arr.length; i++) {
hex += arr[i].toString(16).padStart(2, '0');
}
return BigInt(hex);
}
_padArray(arr, length) {
const res = new Uint8Array(length);
res.set(arr, length - arr.length);
return res;
}
_bigIntToU8Array(bigint, padLength=0) {
let hex = bigint.toString(16);
if (padLength === 0) {
padLength = Math.ceil(hex.length / 2) * 2;
}
hex = hex.padStart(padLength * 2, '0');
const length = hex.length / 2;
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return arr;
}
_modPow(b, e, m) {
if (m === 1n) {
return 0;
}
let r = 1n;
b = b % m;
while (e > 0) {
if (e % 2n === 1n) {
r = (r * b) % m;
}
e = e / 2n;
b = (b * b) % m;
}
return r;
}
async generateKey() {
this._key = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: this._keyLength,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"},
},
true, ["encrypt", "decrypt"]);
const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey);
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
this._nBigInt = this._u8ArrayToBigInt(this._n);
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
this._eBigInt = this._u8ArrayToBigInt(this._e);
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
this._dBigInt = this._u8ArrayToBigInt(this._d);
}
setPublicKey(n, e) {
if (n.length !== this._keyBytes || e.length !== this._keyBytes) {
return;
}
this._n = new Uint8Array(this._keyBytes);
this._e = new Uint8Array(this._keyBytes);
this._n.set(n);
this._e.set(e);
this._nBigInt = this._u8ArrayToBigInt(this._n);
this._eBigInt = this._u8ArrayToBigInt(this._e);
}
encrypt(message) {
if (message.length > this._keyBytes - 11) {
return null;
}
const ps = new Uint8Array(this._keyBytes - message.length - 3);
window.crypto.getRandomValues(ps);
for (let i = 0; i < ps.length; i++) {
ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
}
const em = new Uint8Array(this._keyBytes);
em[1] = 0x02;
em.set(ps, 2);
em.set(message, ps.length + 3);
const emBigInt = this._u8ArrayToBigInt(em);
const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt);
return this._bigIntToU8Array(c, this._keyBytes);
}
decrypt(message) {
if (message.length !== this._keyBytes) {
return null;
}
const msgBigInt = this._u8ArrayToBigInt(message);
const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt);
const em = this._bigIntToU8Array(emBigInt, this._keyBytes);
if (em[0] !== 0x00 || em[1] !== 0x02) {
return null;
}
let i = 2;
for (; i < em.length; i++) {
if (em[i] === 0x00) {
break;
}
}
if (i === em.length) {
return null;
}
return em.slice(i + 1, em.length);
}
get keyLength() {
return this._keyLength;
}
get n() {
return this._n;
}
get e() {
return this._e;
}
get d() {
return this._d;
}
}
export default class RSAAESAuthenticationState extends EventTargetMixin {
constructor(sock, getCredentials) {
super();
@@ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
this._hasStarted = true;
// 1: Receive server public key
await this._waitSockAsync(4);
const serverKeyLengthBuffer = this._sock.rQslice(0, 4);
const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
const serverKeyLength = this._sock.rQshift32();
if (serverKeyLength < 1024) {
throw new Error("RA2: server public key is too short: " + serverKeyLength);
@@ -417,26 +150,31 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
await this._waitSockAsync(serverKeyBytes * 2);
const serverN = this._sock.rQshiftBytes(serverKeyBytes);
const serverE = this._sock.rQshiftBytes(serverKeyBytes);
const serverRSACipher = new RSACipher(serverKeyLength);
serverRSACipher.setPublicKey(serverN, serverE);
const serverRSACipher = await legacyCrypto.importKey(
"raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
serverPublickey.set(serverKeyLengthBuffer);
serverPublickey.set(serverN, 4);
serverPublickey.set(serverE, 4 + serverKeyBytes);
// verify server public key
let approveKey = this._waitApproveKeyAsync();
this.dispatchEvent(new CustomEvent("serververification", {
detail: { type: "RSA", publickey: serverPublickey }
}));
await this._waitApproveKeyAsync();
await approveKey;
// 2: Send client public key
const clientKeyLength = 2048;
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
const clientRSACipher = new RSACipher(clientKeyLength);
await clientRSACipher.generateKey();
const clientN = clientRSACipher.n;
const clientE = clientRSACipher.e;
const clientRSACipher = (await legacyCrypto.generateKey({
name: "RSA-PKCS1-v1_5",
modulusLength: clientKeyLength,
publicExponent: new Uint8Array([1, 0, 1]),
}, true, ["encrypt"])).privateKey;
const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
const clientN = clientExportedRSAKey.n;
const clientE = clientExportedRSAKey.e;
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
@@ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
clientPublicKey[3] = clientKeyLength & 0xff;
clientPublicKey.set(clientN, 4);
clientPublicKey.set(clientE, 4 + clientKeyBytes);
this._sock.send(clientPublicKey);
this._sock.sQpushBytes(clientPublicKey);
this._sock.flush();
// 3: Send client random
const clientRandom = new Uint8Array(16);
window.crypto.getRandomValues(clientRandom);
const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom);
const clientEncryptedRandom = await legacyCrypto.encrypt(
{ name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
clientRandomMessage[1] = serverKeyBytes & 0xff;
clientRandomMessage.set(clientEncryptedRandom, 2);
this._sock.send(clientRandomMessage);
this._sock.sQpushBytes(clientRandomMessage);
this._sock.flush();
// 4: Receive server random
await this._waitSockAsync(2);
@@ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
throw new Error("RA2: wrong encrypted message length");
}
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom);
const serverRandom = await legacyCrypto.decrypt(
{ name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
if (serverRandom === null || serverRandom.length !== 16) {
throw new Error("RA2: corrupted server encrypted random");
}
@@ -494,13 +236,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
serverHash = new Uint8Array(serverHash);
clientHash = new Uint8Array(clientHash);
this._sock.send(await clientCipher.makeMessage(clientHash));
this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));
this._sock.flush();
await this._waitSockAsync(2 + 20 + 16);
if (this._sock.rQshift16() !== 20) {
throw new Error("RA2: wrong server hash");
}
const serverHashReceived = await serverCipher.receiveMessage(
20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16));
20, this._sock.rQshiftBytes(20 + 16));
if (serverHashReceived === null) {
throw new Error("RA2: failed to authenticate the message");
}
@@ -516,11 +259,12 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
throw new Error("RA2: wrong subtype");
}
let subtype = (await serverCipher.receiveMessage(
1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16)));
1, this._sock.rQshiftBytes(1 + 16)));
if (subtype === null) {
throw new Error("RA2: failed to authenticate the message");
}
subtype = subtype[0];
let waitCredentials = this._waitCredentialsAsync(subtype);
if (subtype === 1) {
if (this._getCredentials().username === undefined ||
this._getCredentials().password === undefined) {
@@ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
} else {
throw new Error("RA2: wrong subtype");
}
await this._waitCredentialsAsync(subtype);
await waitCredentials;
let username;
if (subtype === 1) {
username = encodeUTF8(this._getCredentials().username).slice(0, 255);
@@ -554,7 +298,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
for (let i = 0; i < password.length; i++) {
credentials[username.length + 2 + i] = password.charCodeAt(i);
}
this._sock.send(await clientCipher.makeMessage(credentials));
this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));
this._sock.flush();
}
get hasStarted() {
@@ -564,4 +309,4 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
set hasStarted(s) {
this._hasStarted = s;
}
}
}

View File

@@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js";
import GestureHandler from "./input/gesturehandler.js";
import Cursor from "./util/cursor.js";
import Websock from "./websock.js";
import DES from "./des.js";
import KeyTable from "./input/keysym.js";
import XtScancode from "./input/xtscancodes.js";
import { encodings } from "./encodings.js";
import RSAAESAuthenticationState from "./ra2.js";
import { MD5 } from "./util/md5.js";
import legacyCrypto from "./crypto/crypto.js";
import RawDecoder from "./decoders/raw.js";
import CopyRectDecoder from "./decoders/copyrect.js";
@@ -258,10 +257,11 @@ export default class RFB extends EventTargetMixin {
Log.Error("Display exception: " + exc);
throw exc;
}
this._display.onflush = this._onFlush.bind(this);
this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
this._remoteNumLock = null;
this._gestures = new GestureHandler();
@@ -960,7 +960,7 @@ export default class RFB extends EventTargetMixin {
}
_handleMessage() {
if (this._sock.rQlen === 0) {
if (this._sock.rQwait("message", 1)) {
Log.Warn("handleMessage called on an empty receive queue");
return;
}
@@ -977,7 +977,7 @@ export default class RFB extends EventTargetMixin {
if (!this._normalMsg()) {
break;
}
if (this._sock.rQlen === 0) {
if (this._sock.rQwait("message", 1)) {
break;
}
}
@@ -995,7 +995,35 @@ export default class RFB extends EventTargetMixin {
}
}
_handleKeyEvent(keysym, code, down) {
_handleKeyEvent(keysym, code, down, numlock, capslock) {
// If remote state of capslock is known, and it doesn't match the local led state of
// the keyboard, we send a capslock keypress first to bring it into sync.
// If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
// we clear the remote state so that we don't send duplicate or spurious fixes,
// since it may take some time to receive the new remote CapsLock state.
if (code == 'CapsLock' && down) {
this._remoteCapsLock = null;
}
if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
Log.Debug("Fixing remote caps lock");
this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
// We clear the remote capsLock state when we do this to prevent issues with doing this twice
// before we receive an update of the the remote state.
this._remoteCapsLock = null;
}
// Logic for numlock is exactly the same.
if (code == 'NumLock' && down) {
this._remoteNumLock = null;
}
if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
Log.Debug("Fixing remote num lock");
this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
this._remoteNumLock = null;
}
this.sendKey(keysym, code, down);
}
@@ -1383,7 +1411,8 @@ export default class RFB extends EventTargetMixin {
while (repeaterID.length < 250) {
repeaterID += "\0";
}
this._sock.sendString(repeaterID);
this._sock.sQpushString(repeaterID);
this._sock.flush();
return true;
}
@@ -1393,7 +1422,8 @@ export default class RFB extends EventTargetMixin {
const cversion = "00" + parseInt(this._rfbVersion, 10) +
".00" + ((this._rfbVersion * 10) % 10);
this._sock.sendString("RFB " + cversion + "\n");
this._sock.sQpushString("RFB " + cversion + "\n");
this._sock.flush();
Log.Debug('Sent ProtocolVersion: ' + cversion);
this._rfbInitState = 'Security';
@@ -1445,7 +1475,8 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported security types (types: " + types + ")");
}
this._sock.send([this._rfbAuthScheme]);
this._sock.sQpush8(this._rfbAuthScheme);
this._sock.flush();
} else {
// Server decides
if (this._sock.rQwait("security scheme", 4)) { return false; }
@@ -1507,12 +1538,15 @@ export default class RFB extends EventTargetMixin {
return false;
}
const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) +
String.fromCharCode(this._rfbCredentials.target.length) +
this._rfbCredentials.username +
this._rfbCredentials.target;
this._sock.sendString(xvpAuthStr);
this._sock.sQpush8(this._rfbCredentials.username.length);
this._sock.sQpush8(this._rfbCredentials.target.length);
this._sock.sQpushString(this._rfbCredentials.username);
this._sock.sQpushString(this._rfbCredentials.target);
this._sock.flush();
this._rfbAuthScheme = securityTypeVNCAuth;
return this._negotiateAuthentication();
}
@@ -1530,7 +1564,9 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
}
this._sock.send([0, 2]);
this._sock.sQpush8(0);
this._sock.sQpush8(2);
this._sock.flush();
this._rfbVeNCryptState = 1;
}
@@ -1589,12 +1625,10 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported security types (types: " + subtypes + ")");
}
this._sock.send([this._rfbAuthScheme >> 24,
this._rfbAuthScheme >> 16,
this._rfbAuthScheme >> 8,
this._rfbAuthScheme]);
this._sock.sQpush32(this._rfbAuthScheme);
this._sock.flush();
this._rfbVeNCryptState == 4;
this._rfbVeNCryptState = 4;
return true;
}
}
@@ -1611,20 +1645,11 @@ export default class RFB extends EventTargetMixin {
const user = encodeUTF8(this._rfbCredentials.username);
const pass = encodeUTF8(this._rfbCredentials.password);
this._sock.send([
(user.length >> 24) & 0xFF,
(user.length >> 16) & 0xFF,
(user.length >> 8) & 0xFF,
user.length & 0xFF
]);
this._sock.send([
(pass.length >> 24) & 0xFF,
(pass.length >> 16) & 0xFF,
(pass.length >> 8) & 0xFF,
pass.length & 0xFF
]);
this._sock.sendString(user);
this._sock.sendString(pass);
this._sock.sQpush32(user.length);
this._sock.sQpush32(pass.length);
this._sock.sQpushString(user);
this._sock.sQpushString(pass);
this._sock.flush();
this._rfbInitState = "SecurityResult";
return true;
@@ -1643,7 +1668,8 @@ export default class RFB extends EventTargetMixin {
// TODO(directxman12): make genDES not require an Array
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
const response = RFB.genDES(this._rfbCredentials.password, challenge);
this._sock.send(response);
this._sock.sQpushBytes(response);
this._sock.flush();
this._rfbInitState = "SecurityResult";
return true;
}
@@ -1661,8 +1687,9 @@ export default class RFB extends EventTargetMixin {
if (this._rfbCredentials.ardPublicKey != undefined &&
this._rfbCredentials.ardCredentials != undefined) {
// if the async web crypto is done return the results
this._sock.send(this._rfbCredentials.ardCredentials);
this._sock.send(this._rfbCredentials.ardPublicKey);
this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
this._sock.flush();
this._rfbCredentials.ardCredentials = null;
this._rfbCredentials.ardPublicKey = null;
this._rfbInitState = "SecurityResult";
@@ -1681,77 +1708,35 @@ export default class RFB extends EventTargetMixin {
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength));
let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join('');
this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding);
let clientKey = legacyCrypto.generateKey(
{ name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
return false;
}
_modPow(base, exponent, modulus) {
async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
const sharedKey = legacyCrypto.deriveBits(
{ name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
let b = BigInt(baseHex);
let e = BigInt(exponentHex);
let m = BigInt(modulusHex);
let r = 1n;
b = b % m;
while (e > 0) {
if (e % 2n === 1n) {
r = (r * b) % m;
}
e = e / 2n;
b = (b * b) % m;
const credentials = window.crypto.getRandomValues(new Uint8Array(128));
for (let i = 0; i < username.length; i++) {
credentials[i] = username.charCodeAt(i);
}
let hexResult = r.toString(16);
while (hexResult.length/2<exponent.length || (hexResult.length%2 != 0)) {
hexResult = "0"+hexResult;
credentials[username.length] = 0;
for (let i = 0; i < password.length; i++) {
credentials[64 + i] = password.charCodeAt(i);
}
credentials[64 + password.length] = 0;
let bytesResult = [];
for (let c = 0; c < hexResult.length; c += 2) {
bytesResult.push(parseInt(hexResult.substr(c, 2), 16));
}
return bytesResult;
}
async _aesEcbEncrypt(string, key) {
// perform AES-ECB blocks
let keyString = Array.from(key, byte => String.fromCharCode(byte)).join('');
let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]);
let data = new Uint8Array(string.length);
for (let i = 0; i < string.length; ++i) {
data[i] = string.charCodeAt(i);
}
let encrypted = new Uint8Array(data.length);
for (let i=0;i<data.length;i+=16) {
let block = data.slice(i, i+16);
let encryptedBlock = await window.crypto.subtle.encrypt({name: "AES-CBC", iv: block},
aesKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
);
encrypted.set((new Uint8Array(encryptedBlock)).slice(0, 16), i);
}
return encrypted;
}
async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) {
// calculate the DH keys
let clientPublicKey = this._modPow(generator, clientPrivateKey, prime);
let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime);
let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
let paddedUsername = username + '\0' + padding.substring(0, 63);
let paddedPassword = password + '\0' + padding.substring(0, 63);
let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64);
let encrypted = await this._aesEcbEncrypt(credentials, sharedKey);
const key = await legacyCrypto.digest("MD5", sharedKey);
const cipher = await legacyCrypto.importKey(
"raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
this._rfbCredentials.ardCredentials = encrypted;
this._rfbCredentials.ardPublicKey = clientPublicKey;
@@ -1768,10 +1753,12 @@ export default class RFB extends EventTargetMixin {
return false;
}
this._sock.send([0, 0, 0, this._rfbCredentials.username.length]);
this._sock.send([0, 0, 0, this._rfbCredentials.password.length]);
this._sock.sendString(this._rfbCredentials.username);
this._sock.sendString(this._rfbCredentials.password);
this._sock.sQpush32(this._rfbCredentials.username.length);
this._sock.sQpush32(this._rfbCredentials.password.length);
this._sock.sQpushString(this._rfbCredentials.username);
this._sock.sQpushString(this._rfbCredentials.password);
this._sock.flush();
this._rfbInitState = "SecurityResult";
return true;
}
@@ -1809,7 +1796,8 @@ export default class RFB extends EventTargetMixin {
"vendor or signature");
}
Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
this._sock.sQpush32(0); // use NOTUNNEL
this._sock.flush();
return false; // wait until we receive the sub auth count to continue
} else {
return this._fail("Server wanted tunnels, but doesn't support " +
@@ -1859,7 +1847,8 @@ export default class RFB extends EventTargetMixin {
for (let authType in clientSupportedTypes) {
if (serverSupportedTypes.indexOf(authType) != -1) {
this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
this._sock.sQpush32(clientSupportedTypes[authType]);
this._sock.flush();
Log.Debug("Selected authentication type: " + authType);
switch (authType) {
@@ -1905,8 +1894,8 @@ export default class RFB extends EventTargetMixin {
if (e.message !== "disconnect normally") {
this._fail(e.message);
}
}).then(() => {
this.dispatchEvent(new CustomEvent('securityresult'));
})
.then(() => {
this._rfbInitState = "SecurityResult";
return true;
}).finally(() => {
@@ -1934,15 +1923,15 @@ export default class RFB extends EventTargetMixin {
const g = this._sock.rQshiftBytes(8);
const p = this._sock.rQshiftBytes(8);
const A = this._sock.rQshiftBytes(8);
const b = window.crypto.getRandomValues(new Uint8Array(8));
const B = new Uint8Array(this._modPow(g, b, p));
const secret = new Uint8Array(this._modPow(A, b, p));
const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
const des = new DES(secret);
const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
const usernameBytes = new Uint8Array(256);
const passwordBytes = new Uint8Array(64);
let usernameBytes = new Uint8Array(256);
let passwordBytes = new Uint8Array(64);
window.crypto.getRandomValues(usernameBytes);
window.crypto.getRandomValues(passwordBytes);
for (let i = 0; i < username.length; i++) {
@@ -1953,25 +1942,12 @@ export default class RFB extends EventTargetMixin {
passwordBytes[i] = password.charCodeAt(i);
}
passwordBytes[password.length] = 0;
let x = new Uint8Array(secret);
for (let i = 0; i < 32; i++) {
for (let j = 0; j < 8; j++) {
x[j] ^= usernameBytes[i * 8 + j];
}
x = des.enc8(x);
usernameBytes.set(x, i * 8);
}
x = new Uint8Array(secret);
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
x[j] ^= passwordBytes[i * 8 + j];
}
x = des.enc8(x);
passwordBytes.set(x, i * 8);
}
this._sock.send(B);
this._sock.send(usernameBytes);
this._sock.send(passwordBytes);
usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
this._sock.sQpushBytes(B);
this._sock.sQpushBytes(usernameBytes);
this._sock.sQpushBytes(passwordBytes);
this._sock.flush();
this._rfbInitState = "SecurityResult";
return true;
}
@@ -1979,7 +1955,11 @@ export default class RFB extends EventTargetMixin {
_negotiateAuthentication() {
switch (this._rfbAuthScheme) {
case securityTypeNone:
this._rfbInitState = 'SecurityResult';
if (this._rfbVersion >= 3.8) {
this._rfbInitState = 'SecurityResult';
} else {
this._rfbInitState = 'ClientInitialisation';
}
return true;
case securityTypeXVP:
@@ -2016,13 +1996,6 @@ export default class RFB extends EventTargetMixin {
}
_handleSecurityResult() {
// There is no security choice, and hence no security result
// until RFB 3.7
if (this._rfbVersion < 3.7) {
this._rfbInitState = 'ClientInitialisation';
return true;
}
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
const status = this._sock.rQshift32();
@@ -2158,6 +2131,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingDesktopSize);
encs.push(encodings.pseudoEncodingLastRect);
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
encs.push(encodings.pseudoEncodingQEMULedEvent);
encs.push(encodings.pseudoEncodingExtendedDesktopSize);
encs.push(encodings.pseudoEncodingXvp);
encs.push(encodings.pseudoEncodingFence);
@@ -2199,7 +2173,8 @@ export default class RFB extends EventTargetMixin {
return this._handleSecurityReason();
case 'ClientInitialisation':
this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
this._sock.flush();
this._rfbInitState = 'ServerInitialisation';
return true;
@@ -2381,7 +2356,7 @@ export default class RFB extends EventTargetMixin {
textData = textData.slice(0, -1);
}
textData = textData.replace("\r\n", "\n");
textData = textData.replaceAll("\r\n", "\n");
this.dispatchEvent(new CustomEvent(
"clipboard",
@@ -2512,19 +2487,11 @@ export default class RFB extends EventTargetMixin {
default:
this._fail("Unexpected server message (type " + msgType + ")");
Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
return true;
}
}
_onFlush() {
this._flushing = false;
// Resume processing
if (this._sock.rQlen > 0) {
this._handleMessage();
}
}
_framebufferUpdate() {
if (this._FBU.rects === 0) {
if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
@@ -2535,7 +2502,14 @@ export default class RFB extends EventTargetMixin {
// to avoid building up an excessive queue
if (this._display.pending()) {
this._flushing = true;
this._display.flush();
this._display.flush()
.then(() => {
this._flushing = false;
// Resume processing
if (!this._sock.rQwait("message", 1)) {
this._handleMessage();
}
});
return false;
}
}
@@ -2545,13 +2519,13 @@ export default class RFB extends EventTargetMixin {
if (this._sock.rQwait("rect header", 12)) { return false; }
/* New FramebufferUpdate */
const hdr = this._sock.rQshiftBytes(12);
this._FBU.x = (hdr[0] << 8) + hdr[1];
this._FBU.y = (hdr[2] << 8) + hdr[3];
this._FBU.width = (hdr[4] << 8) + hdr[5];
this._FBU.height = (hdr[6] << 8) + hdr[7];
this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
(hdr[10] << 8) + hdr[11], 10);
this._FBU.x = this._sock.rQshift16();
this._FBU.y = this._sock.rQshift16();
this._FBU.width = this._sock.rQshift16();
this._FBU.height = this._sock.rQshift16();
this._FBU.encoding = this._sock.rQshift32();
/* Encodings are signed */
this._FBU.encoding >>= 0;
}
if (!this._handleRect()) {
@@ -2593,6 +2567,9 @@ export default class RFB extends EventTargetMixin {
case encodings.pseudoEncodingExtendedDesktopSize:
return this._handleExtendedDesktopSize();
case encodings.pseudoEncodingQEMULedEvent:
return this._handleLedEvent();
default:
return this._handleDataRect();
}
@@ -2770,6 +2747,21 @@ export default class RFB extends EventTargetMixin {
return true;
}
_handleLedEvent() {
if (this._sock.rQwait("LED Status", 1)) {
return false;
}
let data = this._sock.rQshift8();
// ScrollLock state can be retrieved with data & 1. This is currently not needed.
let numLock = data & 2 ? true : false;
let capsLock = data & 4 ? true : false;
this._remoteCapsLock = capsLock;
this._remoteNumLock = numLock;
return true;
}
_handleExtendedDesktopSize() {
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
return false;
@@ -2785,26 +2777,18 @@ export default class RFB extends EventTargetMixin {
const firstUpdate = !this._supportsSetDesktopSize;
this._supportsSetDesktopSize = true;
// Normally we only apply the current resize mode after a
// window resize event. However there is no such trigger on the
// initial connect. And we don't know if the server supports
// resizing until we've gotten here.
if (firstUpdate) {
this._requestRemoteResize();
}
this._sock.rQskipBytes(1); // number-of-screens
this._sock.rQskipBytes(3); // padding
for (let i = 0; i < numberOfScreens; i += 1) {
// Save the id and flags of the first screen
if (i === 0) {
this._screenID = this._sock.rQshiftBytes(4); // id
this._sock.rQskipBytes(2); // x-position
this._sock.rQskipBytes(2); // y-position
this._sock.rQskipBytes(2); // width
this._sock.rQskipBytes(2); // height
this._screenFlags = this._sock.rQshiftBytes(4); // flags
this._screenID = this._sock.rQshift32(); // id
this._sock.rQskipBytes(2); // x-position
this._sock.rQskipBytes(2); // y-position
this._sock.rQskipBytes(2); // width
this._sock.rQskipBytes(2); // height
this._screenFlags = this._sock.rQshift32(); // flags
} else {
this._sock.rQskipBytes(16);
}
@@ -2842,6 +2826,14 @@ export default class RFB extends EventTargetMixin {
this._resize(this._FBU.width, this._FBU.height);
}
// Normally we only apply the current resize mode after a
// window resize event. However there is no such trigger on the
// initial connect. And we don't know if the server supports
// resizing until we've gotten here.
if (firstUpdate) {
this._requestRemoteResize();
}
return true;
}
@@ -2937,28 +2929,22 @@ export default class RFB extends EventTargetMixin {
static genDES(password, challenge) {
const passwordChars = password.split('').map(c => c.charCodeAt(0));
return (new DES(passwordChars)).encrypt(challenge);
const key = legacyCrypto.importKey(
"raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
}
}
// Class Methods
RFB.messages = {
keyEvent(sock, keysym, down) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(4); // msg-type
sock.sQpush8(down);
buff[offset] = 4; // msg-type
buff[offset + 1] = down;
sock.sQpush16(0);
buff[offset + 2] = 0;
buff[offset + 3] = 0;
sock.sQpush32(keysym);
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
sock._sQlen += 8;
sock.flush();
},
@@ -2972,46 +2958,28 @@ RFB.messages = {
return xtScanCode;
}
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(255); // msg-type
sock.sQpush8(0); // sub msg-type
buff[offset] = 255; // msg-type
buff[offset + 1] = 0; // sub msg-type
sock.sQpush16(down);
buff[offset + 2] = (down >> 8);
buff[offset + 3] = down;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
sock.sQpush32(keysym);
const RFBkeycode = getRFBkeycode(keycode);
buff[offset + 8] = (RFBkeycode >> 24);
buff[offset + 9] = (RFBkeycode >> 16);
buff[offset + 10] = (RFBkeycode >> 8);
buff[offset + 11] = RFBkeycode;
sock.sQpush32(RFBkeycode);
sock._sQlen += 12;
sock.flush();
},
pointerEvent(sock, x, y, mask) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(5); // msg-type
buff[offset] = 5; // msg-type
sock.sQpush8(mask);
buff[offset + 1] = mask;
sock.sQpush16(x);
sock.sQpush16(y);
buff[offset + 2] = x >> 8;
buff[offset + 3] = x;
buff[offset + 4] = y >> 8;
buff[offset + 5] = y;
sock._sQlen += 6;
sock.flush();
},
@@ -3111,14 +3079,11 @@ RFB.messages = {
},
clientCutText(sock, data, extended = false) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(6); // msg-type
buff[offset] = 6; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
let length;
if (extended) {
@@ -3127,121 +3092,63 @@ RFB.messages = {
length = data.length;
}
buff[offset + 4] = length >> 24;
buff[offset + 5] = length >> 16;
buff[offset + 6] = length >> 8;
buff[offset + 7] = length;
sock._sQlen += 8;
// We have to keep track of from where in the data we begin creating the
// buffer for the flush in the next iteration.
let dataOffset = 0;
let remaining = data.length;
while (remaining > 0) {
let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
for (let i = 0; i < flushSize; i++) {
buff[sock._sQlen + i] = data[dataOffset + i];
}
sock._sQlen += flushSize;
sock.flush();
remaining -= flushSize;
dataOffset += flushSize;
}
sock.sQpush32(length);
sock.sQpushBytes(data);
sock.flush();
},
setDesktopSize(sock, width, height, id, flags) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(251); // msg-type
buff[offset] = 251; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = width >> 8; // width
buff[offset + 3] = width;
buff[offset + 4] = height >> 8; // height
buff[offset + 5] = height;
sock.sQpush8(0); // padding
buff[offset + 6] = 1; // number-of-screens
buff[offset + 7] = 0; // padding
sock.sQpush16(width);
sock.sQpush16(height);
sock.sQpush8(1); // number-of-screens
sock.sQpush8(0); // padding
// screen array
buff[offset + 8] = id >> 24; // id
buff[offset + 9] = id >> 16;
buff[offset + 10] = id >> 8;
buff[offset + 11] = id;
buff[offset + 12] = 0; // x-position
buff[offset + 13] = 0;
buff[offset + 14] = 0; // y-position
buff[offset + 15] = 0;
buff[offset + 16] = width >> 8; // width
buff[offset + 17] = width;
buff[offset + 18] = height >> 8; // height
buff[offset + 19] = height;
buff[offset + 20] = flags >> 24; // flags
buff[offset + 21] = flags >> 16;
buff[offset + 22] = flags >> 8;
buff[offset + 23] = flags;
sock.sQpush32(id);
sock.sQpush16(0); // x-position
sock.sQpush16(0); // y-position
sock.sQpush16(width);
sock.sQpush16(height);
sock.sQpush32(flags);
sock._sQlen += 24;
sock.flush();
},
clientFence(sock, flags, payload) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(248); // msg-type
buff[offset] = 248; // msg-type
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
sock.sQpush32(flags);
buff[offset + 4] = flags >> 24; // flags
buff[offset + 5] = flags >> 16;
buff[offset + 6] = flags >> 8;
buff[offset + 7] = flags;
sock.sQpush8(payload.length);
sock.sQpushString(payload);
const n = payload.length;
buff[offset + 8] = n; // length
for (let i = 0; i < n; i++) {
buff[offset + 9 + i] = payload.charCodeAt(i);
}
sock._sQlen += 9 + n;
sock.flush();
},
enableContinuousUpdates(sock, enable, x, y, width, height) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(150); // msg-type
buff[offset] = 150; // msg-type
buff[offset + 1] = enable; // enable-flag
sock.sQpush8(enable);
buff[offset + 2] = x >> 8; // x
buff[offset + 3] = x;
buff[offset + 4] = y >> 8; // y
buff[offset + 5] = y;
buff[offset + 6] = width >> 8; // width
buff[offset + 7] = width;
buff[offset + 8] = height >> 8; // height
buff[offset + 9] = height;
sock.sQpush16(x);
sock.sQpush16(y);
sock.sQpush16(width);
sock.sQpush16(height);
sock._sQlen += 10;
sock.flush();
},
pixelFormat(sock, depth, trueColor) {
const buff = sock._sQ;
const offset = sock._sQlen;
let bpp;
if (depth > 16) {
@@ -3254,100 +3161,69 @@ RFB.messages = {
const bits = Math.floor(depth/3);
buff[offset] = 0; // msg-type
sock.sQpush8(0); // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 4] = bpp; // bits-per-pixel
buff[offset + 5] = depth; // depth
buff[offset + 6] = 0; // little-endian
buff[offset + 7] = trueColor ? 1 : 0; // true-color
sock.sQpush8(bpp);
sock.sQpush8(depth);
sock.sQpush8(0); // little-endian
sock.sQpush8(trueColor ? 1 : 0);
buff[offset + 8] = 0; // red-max
buff[offset + 9] = (1 << bits) - 1; // red-max
sock.sQpush16((1 << bits) - 1); // red-max
sock.sQpush16((1 << bits) - 1); // green-max
sock.sQpush16((1 << bits) - 1); // blue-max
buff[offset + 10] = 0; // green-max
buff[offset + 11] = (1 << bits) - 1; // green-max
sock.sQpush8(bits * 0); // red-shift
sock.sQpush8(bits * 1); // green-shift
sock.sQpush8(bits * 2); // blue-shift
buff[offset + 12] = 0; // blue-max
buff[offset + 13] = (1 << bits) - 1; // blue-max
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 14] = bits * 0; // red-shift
buff[offset + 15] = bits * 1; // green-shift
buff[offset + 16] = bits * 2; // blue-shift
buff[offset + 17] = 0; // padding
buff[offset + 18] = 0; // padding
buff[offset + 19] = 0; // padding
sock._sQlen += 20;
sock.flush();
},
clientEncodings(sock, encodings) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(2); // msg-type
buff[offset] = 2; // msg-type
buff[offset + 1] = 0; // padding
sock.sQpush8(0); // padding
buff[offset + 2] = encodings.length >> 8;
buff[offset + 3] = encodings.length;
let j = offset + 4;
sock.sQpush16(encodings.length);
for (let i = 0; i < encodings.length; i++) {
const enc = encodings[i];
buff[j] = enc >> 24;
buff[j + 1] = enc >> 16;
buff[j + 2] = enc >> 8;
buff[j + 3] = enc;
j += 4;
sock.sQpush32(encodings[i]);
}
sock._sQlen += j - offset;
sock.flush();
},
fbUpdateRequest(sock, incremental, x, y, w, h) {
const buff = sock._sQ;
const offset = sock._sQlen;
if (typeof(x) === "undefined") { x = 0; }
if (typeof(y) === "undefined") { y = 0; }
buff[offset] = 3; // msg-type
buff[offset + 1] = incremental ? 1 : 0;
sock.sQpush8(3); // msg-type
buff[offset + 2] = (x >> 8) & 0xFF;
buff[offset + 3] = x & 0xFF;
sock.sQpush8(incremental ? 1 : 0);
buff[offset + 4] = (y >> 8) & 0xFF;
buff[offset + 5] = y & 0xFF;
sock.sQpush16(x);
sock.sQpush16(y);
sock.sQpush16(w);
sock.sQpush16(h);
buff[offset + 6] = (w >> 8) & 0xFF;
buff[offset + 7] = w & 0xFF;
buff[offset + 8] = (h >> 8) & 0xFF;
buff[offset + 9] = h & 0xFF;
sock._sQlen += 10;
sock.flush();
},
xvpOp(sock, ver, op) {
const buff = sock._sQ;
const offset = sock._sQlen;
sock.sQpush8(250); // msg-type
buff[offset] = 250; // msg-type
buff[offset + 1] = 0; // padding
sock.sQpush8(0); // padding
buff[offset + 2] = ver;
buff[offset + 3] = op;
sock.sQpush8(ver);
sock.sQpush8(op);
sock._sQlen += 4;
sock.flush();
}
};

View File

@@ -69,7 +69,9 @@ export default class Cursor {
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
document.body.removeChild(this._canvas);
if (document.contains(this._canvas)) {
document.body.removeChild(this._canvas);
}
}
this._target = null;

View File

@@ -94,27 +94,7 @@ export default class Websock {
return "unknown";
}
get sQ() {
return this._sQ;
}
get rQ() {
return this._rQ;
}
get rQi() {
return this._rQi;
}
set rQi(val) {
this._rQi = val;
}
// Receive Queue
get rQlen() {
return this._rQlen - this._rQi;
}
rQpeek8() {
return this._rQ[this._rQi];
}
@@ -141,42 +121,47 @@ export default class Websock {
for (let byte = bytes - 1; byte >= 0; byte--) {
res += this._rQ[this._rQi++] << (byte * 8);
}
return res;
return res >>> 0;
}
rQshiftStr(len) {
if (typeof(len) === 'undefined') { len = this.rQlen; }
let str = "";
// Handle large arrays in steps to avoid long strings on the stack
for (let i = 0; i < len; i += 4096) {
let part = this.rQshiftBytes(Math.min(4096, len - i));
let part = this.rQshiftBytes(Math.min(4096, len - i), false);
str += String.fromCharCode.apply(null, part);
}
return str;
}
rQshiftBytes(len) {
if (typeof(len) === 'undefined') { len = this.rQlen; }
rQshiftBytes(len, copy=true) {
this._rQi += len;
return new Uint8Array(this._rQ.buffer, this._rQi - len, len);
if (copy) {
return this._rQ.slice(this._rQi - len, this._rQi);
} else {
return this._rQ.subarray(this._rQi - len, this._rQi);
}
}
rQshiftTo(target, len) {
if (len === undefined) { len = this.rQlen; }
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ
target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
this._rQi += len;
}
rQslice(start, end = this.rQlen) {
return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start);
rQpeekBytes(len, copy=true) {
if (copy) {
return this._rQ.slice(this._rQi, this._rQi + len);
} else {
return this._rQ.subarray(this._rQi, this._rQi + len);
}
}
// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
// to be available in the receive queue. Return true if we need to
// wait (and possibly print a debug message), otherwise false.
rQwait(msg, num, goback) {
if (this.rQlen < num) {
if (this._rQlen - this._rQi < num) {
if (goback) {
if (this._rQi < goback) {
throw new Error("rQwait cannot backup " + goback + " bytes");
@@ -190,21 +175,56 @@ export default class Websock {
// Send Queue
sQpush8(num) {
this._sQensureSpace(1);
this._sQ[this._sQlen++] = num;
}
sQpush16(num) {
this._sQensureSpace(2);
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
}
sQpush32(num) {
this._sQensureSpace(4);
this._sQ[this._sQlen++] = (num >> 24) & 0xff;
this._sQ[this._sQlen++] = (num >> 16) & 0xff;
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
}
sQpushString(str) {
let bytes = str.split('').map(chr => chr.charCodeAt(0));
this.sQpushBytes(new Uint8Array(bytes));
}
sQpushBytes(bytes) {
for (let offset = 0;offset < bytes.length;) {
this._sQensureSpace(1);
let chunkSize = this._sQbufferSize - this._sQlen;
if (chunkSize > bytes.length - offset) {
chunkSize = bytes.length - offset;
}
this._sQ.set(bytes.subarray(offset, chunkSize), this._sQlen);
this._sQlen += chunkSize;
offset += chunkSize;
}
}
flush() {
if (this._sQlen > 0 && this.readyState === 'open') {
this._websocket.send(this._encodeMessage());
this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen));
this._sQlen = 0;
}
}
send(arr) {
this._sQ.set(arr, this._sQlen);
this._sQlen += arr.length;
this.flush();
}
sendString(str) {
this.send(str.split('').map(chr => chr.charCodeAt(0)));
_sQensureSpace(bytes) {
if (this._sQbufferSize - this._sQlen < bytes) {
this.flush();
}
}
// Event Handlers
@@ -283,17 +303,12 @@ export default class Websock {
}
// private methods
_encodeMessage() {
// Put in a binary arraybuffer
// according to the spec, you can send ArrayBufferViews with the send method
return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
}
// We want to move all the unread data to the start of the queue,
// e.g. compacting.
// The function also expands the receive que if needed, and for
// performance reasons we combine these two actions to avoid
// unneccessary copying.
// unnecessary copying.
_expandCompactRQ(minFit) {
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
// instead of resizing
@@ -309,7 +324,7 @@ export default class Websock {
// we don't want to grow unboundedly
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
this._rQbufferSize = MAX_RQ_GROW_SIZE;
if (this._rQbufferSize - this.rQlen < minFit) {
if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
}
}
@@ -327,25 +342,22 @@ export default class Websock {
}
// push arraybuffer values onto the end of the receive que
_DecodeMessage(data) {
const u8 = new Uint8Array(data);
_recvMessage(e) {
if (this._rQlen == this._rQi) {
// All data has now been processed, this means we
// can reset the receive queue.
this._rQlen = 0;
this._rQi = 0;
}
const u8 = new Uint8Array(e.data);
if (u8.length > this._rQbufferSize - this._rQlen) {
this._expandCompactRQ(u8.length);
}
this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length;
}
_recvMessage(e) {
this._DecodeMessage(e.data);
if (this.rQlen > 0) {
if (this._rQlen - this._rQi > 0) {
this._eventHandlers.message();
if (this._rQlen == this._rQi) {
// All data has now been processed, this means we
// can reset the receive queue.
this._rQlen = 0;
this._rQi = 0;
}
} else {
Log.Debug("Ignoring empty message");
}

View File

@@ -81,9 +81,3 @@ None
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display
| drawImage | (img, x, y) | Draw image and track damage
| autoscale | (containerWidth, containerHeight) | Scale the display
### 2.2.3 Callbacks
| name | parameters | description
| ------- | ---------- | ------------
| onflush | () | A display flush has been requested and we are now ready to resume FBU processing

View File

@@ -3,13 +3,13 @@
.SH NAME
novnc_proxy - noVNC proxy server
.SH SYNOPSIS
.B novnc_proxy [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]
.B novnc_proxy [--listen [HOST:]PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]
Starts the WebSockets proxy and a mini-webserver and
provides a cut-and-paste URL to go to.
--listen PORT Port for proxy/webserver to listen on
Default: 6080
--listen [HOST:]PORT Port for proxy/webserver to listen on
Default: 6080 (on all interfaces)
--vnc VNC_HOST:PORT VNC server host:port proxy target
Default: localhost:5900
--cert CERT Path to combined cert/key file, or just

102
eslint.config.mjs Normal file
View File

@@ -0,0 +1,102 @@
import globals from "globals";
import js from "@eslint/js";
export default [
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
globals: {
...globals.browser,
...globals.es2020,
}
},
ignores: ["**/xtscancodes.js"],
rules: {
// Unsafe or confusing stuff that we forbid
"no-unused-vars": ["error", { "vars": "all",
"args": "none",
"ignoreRestSiblings": true,
"caughtErrors": "none" }],
"no-constant-condition": ["error", { "checkLoops": false }],
"no-var": "error",
"no-useless-constructor": "error",
"object-shorthand": ["error", "methods", { "avoidQuotes": true }],
"prefer-arrow-callback": "error",
"arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": false } ],
"arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }],
"arrow-spacing": ["error"],
"no-confusing-arrow": ["error", { "allowParens": true }],
// Enforced coding style
"brace-style": ["error", "1tbs", { "allowSingleLine": true }],
"indent": ["error", 4, { "SwitchCase": 1,
"VariableDeclarator": "first",
"FunctionDeclaration": { "parameters": "first" },
"FunctionExpression": { "parameters": "first" },
"CallExpression": { "arguments": "first" },
"ArrayExpression": "first",
"ObjectExpression": "first",
"ImportDeclaration": "first",
"ignoreComments": true }],
"comma-spacing": ["error"],
"comma-style": ["error"],
"curly": ["error", "multi-line"],
"func-call-spacing": ["error"],
"func-names": ["error"],
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"key-spacing": ["error"],
"keyword-spacing": ["error"],
"no-trailing-spaces": ["error"],
"semi": ["error"],
"space-before-blocks": ["error"],
"space-before-function-paren": ["error", { "anonymous": "always",
"named": "never",
"asyncArrow": "always" }],
"switch-colon-spacing": ["error"],
"camelcase": ["error", { "allow": ["^XK_", "^XF86XK_"] }],
"no-console": ["error"],
}
},
{
files: ["po/po2js", "po/xgettext-html"],
languageOptions: {
globals: {
...globals.node,
}
},
rules: {
"no-console": 0,
},
},
{
files: ["tests/*"],
languageOptions: {
globals: {
...globals.node,
...globals.mocha,
sinon: false,
chai: false,
}
},
rules: {
"prefer-arrow-callback": 0,
// Too many anonymous callbacks
"func-names": "off",
},
},
{
files: ["utils/*"],
languageOptions: {
globals: {
...globals.node,
}
},
rules: {
"no-console": 0,
},
},
];

View File

@@ -81,5 +81,12 @@ module.exports = (config) => {
singleRun: true,
};
if (process.env.TEST_BROWSER_NAME === 'ChromeHeadless') {
let os = require('os');
if (os.platform() === 'win32') {
my_conf.client.mocha['timeout'] = 5000;
}
}
config.set(my_conf);
};

View File

@@ -1,6 +1,6 @@
{
"name": "@novnc/novnc",
"version": "1.4.0",
"version": "1.5.0-beta",
"description": "An HTML5 VNC client",
"browser": "lib/rfb",
"directories": {
@@ -14,9 +14,7 @@
"VERSION",
"docs/API.md",
"docs/LIBRARY.md",
"docs/LICENSE*",
"core",
"vendor/pako"
"docs/LICENSE*"
],
"scripts": {
"lint": "eslint app core po/po2js po/xgettext-html tests utils",
@@ -39,19 +37,14 @@
"homepage": "https://github.com/novnc/noVNC",
"devDependencies": {
"@babel/core": "latest",
"@babel/plugin-syntax-dynamic-import": "latest",
"@babel/plugin-transform-modules-commonjs": "latest",
"@babel/preset-env": "latest",
"@babel/cli": "latest",
"babel-plugin-import-redirect": "latest",
"browserify": "latest",
"babelify": "latest",
"core-js": "latest",
"chai": "latest",
"commander": "latest",
"es-module-loader": "latest",
"eslint": "latest",
"fs-extra": "latest",
"globals": "latest",
"jsdom": "latest",
"karma": "latest",
"karma-mocha": "latest",

View File

@@ -1,5 +0,0 @@
{
"env": {
"node": true,
},
}

262
po/el.po
View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: noVNC 0.6.1\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2017-11-17 21:40+0200\n"
"POT-Creation-Date: 2022-12-27 15:24+0100\n"
"PO-Revision-Date: 2017-10-11 16:16+0200\n"
"Last-Translator: Giannis Kosmas <kosmasgiannis@gmail.com>\n"
"Language-Team: none\n"
@@ -17,273 +17,349 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: ../app/ui.js:404
#: ../app/ui.js:69
msgid "HTTPS is required for full functionality"
msgstr "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα"
#: ../app/ui.js:410
msgid "Connecting..."
msgstr "Συνδέεται..."
#: ../app/ui.js:411
#: ../app/ui.js:417
msgid "Disconnecting..."
msgstr "Aποσυνδέεται..."
#: ../app/ui.js:417
#: ../app/ui.js:423
msgid "Reconnecting..."
msgstr "Επανασυνδέεται..."
#: ../app/ui.js:422
#: ../app/ui.js:428
msgid "Internal error"
msgstr "Εσωτερικό σφάλμα"
#: ../app/ui.js:1019
#: ../app/ui.js:1026
msgid "Must set host"
msgstr "Πρέπει να οριστεί ο διακομιστής"
#: ../app/ui.js:1099
#: ../app/ui.js:1110
msgid "Connected (encrypted) to "
msgstr "Συνδέθηκε (κρυπτογραφημένα) με το "
#: ../app/ui.js:1101
#: ../app/ui.js:1112
msgid "Connected (unencrypted) to "
msgstr "Συνδέθηκε (μη κρυπτογραφημένα) με το "
#: ../app/ui.js:1119
#: ../app/ui.js:1135
msgid "Something went wrong, connection is closed"
msgstr "Κάτι πήγε στραβά, η σύνδεση διακόπηκε"
#: ../app/ui.js:1129
#: ../app/ui.js:1138
msgid "Failed to connect to server"
msgstr "Αποτυχία στη σύνδεση με το διακομιστή"
#: ../app/ui.js:1150
msgid "Disconnected"
msgstr "Αποσυνδέθηκε"
#: ../app/ui.js:1142
#: ../app/ui.js:1165
msgid "New connection has been rejected with reason: "
msgstr "Η νέα σύνδεση απορρίφθηκε διότι: "
#: ../app/ui.js:1145
#: ../app/ui.js:1168
msgid "New connection has been rejected"
msgstr "Η νέα σύνδεση απορρίφθηκε "
#: ../app/ui.js:1166
msgid "Password is required"
msgstr "Απαιτείται ο κωδικός πρόσβασης"
#: ../app/ui.js:1234
msgid "Credentials are required"
msgstr "Απαιτούνται διαπιστευτήρια"
#: ../vnc.html:89
#: ../vnc.html:57
msgid "noVNC encountered an error:"
msgstr "το noVNC αντιμετώπισε ένα σφάλμα:"
#: ../vnc.html:99
#: ../vnc.html:67
msgid "Hide/Show the control bar"
msgstr "Απόκρυψη/Εμφάνιση γραμμής ελέγχου"
#: ../vnc.html:106
#: ../vnc.html:76
msgid "Drag"
msgstr "Σύρσιμο"
#: ../vnc.html:76
msgid "Move/Drag Viewport"
msgstr "Μετακίνηση/Σύρσιμο Θεατού πεδίου"
#: ../vnc.html:106
msgid "viewport drag"
msgstr "σύρσιμο θεατού πεδίου"
#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
msgid "Active Mouse Button"
msgstr "Ενεργό Πλήκτρο Ποντικιού"
#: ../vnc.html:112
msgid "No mousebutton"
msgstr "Χωρίς Πλήκτρο Ποντικιού"
#: ../vnc.html:115
msgid "Left mousebutton"
msgstr "Αριστερό Πλήκτρο Ποντικιού"
#: ../vnc.html:118
msgid "Middle mousebutton"
msgstr "Μεσαίο Πλήκτρο Ποντικιού"
#: ../vnc.html:121
msgid "Right mousebutton"
msgstr "Δεξί Πλήκτρο Ποντικιού"
#: ../vnc.html:124
#: ../vnc.html:82
msgid "Keyboard"
msgstr "Πληκτρολόγιο"
#: ../vnc.html:124
#: ../vnc.html:82
msgid "Show Keyboard"
msgstr "Εμφάνιση Πληκτρολογίου"
#: ../vnc.html:131
#: ../vnc.html:87
msgid "Extra keys"
msgstr "Επιπλέον πλήκτρα"
#: ../vnc.html:131
#: ../vnc.html:87
msgid "Show Extra Keys"
msgstr "Εμφάνιση Επιπλέον Πλήκτρων"
#: ../vnc.html:136
#: ../vnc.html:92
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:136
#: ../vnc.html:92
msgid "Toggle Ctrl"
msgstr "Εναλλαγή Ctrl"
#: ../vnc.html:139
#: ../vnc.html:95
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:139
#: ../vnc.html:95
msgid "Toggle Alt"
msgstr "Εναλλαγή Alt"
#: ../vnc.html:142
#: ../vnc.html:98
msgid "Toggle Windows"
msgstr "Εναλλαγή Παράθυρων"
#: ../vnc.html:98
msgid "Windows"
msgstr "Παράθυρα"
#: ../vnc.html:101
msgid "Send Tab"
msgstr "Αποστολή Tab"
#: ../vnc.html:142
#: ../vnc.html:101
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:145
#: ../vnc.html:104
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:145
#: ../vnc.html:104
msgid "Send Escape"
msgstr "Αποστολή Escape"
#: ../vnc.html:148
#: ../vnc.html:107
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:148
#: ../vnc.html:107
msgid "Send Ctrl-Alt-Del"
msgstr "Αποστολή Ctrl-Alt-Del"
#: ../vnc.html:156
#: ../vnc.html:114
msgid "Shutdown/Reboot"
msgstr "Κλείσιμο/Επανεκκίνηση"
#: ../vnc.html:156
#: ../vnc.html:114
msgid "Shutdown/Reboot..."
msgstr "Κλείσιμο/Επανεκκίνηση..."
#: ../vnc.html:162
#: ../vnc.html:120
msgid "Power"
msgstr "Απενεργοποίηση"
#: ../vnc.html:164
#: ../vnc.html:122
msgid "Shutdown"
msgstr "Κλείσιμο"
#: ../vnc.html:165
#: ../vnc.html:123
msgid "Reboot"
msgstr "Επανεκκίνηση"
#: ../vnc.html:166
#: ../vnc.html:124
msgid "Reset"
msgstr "Επαναφορά"
#: ../vnc.html:171 ../vnc.html:177
#: ../vnc.html:129 ../vnc.html:135
msgid "Clipboard"
msgstr "Πρόχειρο"
#: ../vnc.html:181
msgid "Clear"
msgstr "Καθάρισμα"
#: ../vnc.html:137
msgid "Edit clipboard content in the textarea below."
msgstr "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω."
#: ../vnc.html:187
msgid "Fullscreen"
#: ../vnc.html:145
#, fuzzy
msgid "Full Screen"
msgstr "Πλήρης Οθόνη"
#: ../vnc.html:192 ../vnc.html:199
#: ../vnc.html:150 ../vnc.html:156
msgid "Settings"
msgstr "Ρυθμίσεις"
#: ../vnc.html:202
#: ../vnc.html:160
msgid "Shared Mode"
msgstr "Κοινόχρηστη Λειτουργία"
#: ../vnc.html:205
#: ../vnc.html:163
msgid "View Only"
msgstr "Μόνο Θέαση"
#: ../vnc.html:209
#: ../vnc.html:167
msgid "Clip to Window"
msgstr "Αποκοπή στο όριο του Παράθυρου"
#: ../vnc.html:212
#: ../vnc.html:170
msgid "Scaling Mode:"
msgstr "Λειτουργία Κλιμάκωσης:"
#: ../vnc.html:214
#: ../vnc.html:172
msgid "None"
msgstr "Καμία"
#: ../vnc.html:215
#: ../vnc.html:173
msgid "Local Scaling"
msgstr "Τοπική Κλιμάκωση"
#: ../vnc.html:216
#: ../vnc.html:174
msgid "Remote Resizing"
msgstr "Απομακρυσμένη Αλλαγή μεγέθους"
#: ../vnc.html:221
#: ../vnc.html:179
msgid "Advanced"
msgstr "Για προχωρημένους"
#: ../vnc.html:224
#: ../vnc.html:182
msgid "Quality:"
msgstr "Ποιότητα:"
#: ../vnc.html:186
msgid "Compression level:"
msgstr "Επίπεδο συμπίεσης:"
#: ../vnc.html:191
msgid "Repeater ID:"
msgstr "Repeater ID:"
#: ../vnc.html:228
#: ../vnc.html:195
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:231
#: ../vnc.html:198
msgid "Encrypt"
msgstr "Κρυπτογράφηση"
#: ../vnc.html:234
#: ../vnc.html:201
msgid "Host:"
msgstr "Όνομα διακομιστή:"
#: ../vnc.html:238
#: ../vnc.html:205
msgid "Port:"
msgstr "Πόρτα διακομιστή:"
#: ../vnc.html:242
#: ../vnc.html:209
msgid "Path:"
msgstr "Διαδρομή:"
#: ../vnc.html:249
#: ../vnc.html:216
msgid "Automatic Reconnect"
msgstr "Αυτόματη επανασύνδεση"
#: ../vnc.html:252
#: ../vnc.html:219
msgid "Reconnect Delay (ms):"
msgstr "Καθυστέρηση επανασύνδεσης (ms):"
#: ../vnc.html:258
#: ../vnc.html:224
msgid "Show Dot when No Cursor"
msgstr "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας"
#: ../vnc.html:229
msgid "Logging:"
msgstr "Καταγραφή:"
#: ../vnc.html:270
#: ../vnc.html:238
msgid "Version:"
msgstr "Έκδοση:"
#: ../vnc.html:246
msgid "Disconnect"
msgstr "Αποσύνδεση"
#: ../vnc.html:289
#: ../vnc.html:269
msgid "Connect"
msgstr "Σύνδεση"
#: ../vnc.html:299
#: ../vnc.html:278
msgid "Server identity"
msgstr "Ταυτότητα Διακομιστή"
#: ../vnc.html:281
msgid "The server has provided the following identifying information:"
msgstr "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:"
#: ../vnc.html:285
msgid "Fingerprint:"
msgstr "Δακτυλικό αποτύπωμα:"
#: ../vnc.html:288
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". "
"Αλλιώς πιέστε \"Απόρριψη\"."
#: ../vnc.html:293
msgid "Approve"
msgstr "Αποδοχή"
#: ../vnc.html:294
msgid "Reject"
msgstr "Απόρριψη"
#: ../vnc.html:302
msgid "Credentials"
msgstr "Διαπιστευτήρια"
#: ../vnc.html:306
msgid "Username:"
msgstr "Κωδικός Χρήστη:"
#: ../vnc.html:310
msgid "Password:"
msgstr "Κωδικός Πρόσβασης:"
#: ../vnc.html:313
#: ../vnc.html:314
msgid "Send Credentials"
msgstr "Αποστολή Διαπιστευτηρίων"
#: ../vnc.html:323
msgid "Cancel"
msgstr "Ακύρωση"
#: ../vnc.html:329
msgid "Canvas not supported."
msgstr "Δεν υποστηρίζεται το στοιχείο Canvas"
#~ msgid "Password is required"
#~ msgstr "Απαιτείται ο κωδικός πρόσβασης"
#~ msgid "viewport drag"
#~ msgstr "σύρσιμο θεατού πεδίου"
#~ msgid "Active Mouse Button"
#~ msgstr "Ενεργό Πλήκτρο Ποντικιού"
#~ msgid "No mousebutton"
#~ msgstr "Χωρίς Πλήκτρο Ποντικιού"
#~ msgid "Left mousebutton"
#~ msgstr "Αριστερό Πλήκτρο Ποντικιού"
#~ msgid "Middle mousebutton"
#~ msgstr "Μεσαίο Πλήκτρο Ποντικιού"
#~ msgid "Right mousebutton"
#~ msgstr "Δεξί Πλήκτρο Ποντικιού"
#~ msgid "Clear"
#~ msgstr "Καθάρισμα"
#~ msgid "Canvas not supported."
#~ msgstr "Δεν υποστηρίζεται το στοιχείο Canvas"
#~ msgid "Disconnect timeout"
#~ msgstr "Παρέλευση χρονικού ορίου αποσύνδεσης"

199
po/ja.po
View File

@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.1.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2020-07-03 16:11+0200\n"
"PO-Revision-Date: 2021-01-15 12:37+0900\n"
"POT-Creation-Date: 2022-12-27 15:24+0100\n"
"PO-Revision-Date: 2023-03-21 12:42+0900\n"
"Last-Translator: nnn1590 <nnn1590@nnn1590.org>\n"
"Language-Team: Japanese\n"
"Language: ja\n"
@@ -19,286 +19,325 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 2.3\n"
#: ../app/ui.js:394
#: ../app/ui.js:69
msgid "HTTPS is required for full functionality"
msgstr "すべての機能を使用するにはHTTPS接続が必要です"
#: ../app/ui.js:410
msgid "Connecting..."
msgstr "接続しています..."
#: ../app/ui.js:401
#: ../app/ui.js:417
msgid "Disconnecting..."
msgstr "切断しています..."
#: ../app/ui.js:407
#: ../app/ui.js:423
msgid "Reconnecting..."
msgstr "再接続しています..."
#: ../app/ui.js:412
#: ../app/ui.js:428
msgid "Internal error"
msgstr "内部エラー"
#: ../app/ui.js:1008
#: ../app/ui.js:1026
msgid "Must set host"
msgstr "ホストを設定する必要があります"
#: ../app/ui.js:1090
#: ../app/ui.js:1110
msgid "Connected (encrypted) to "
msgstr "接続しました (暗号化済み): "
#: ../app/ui.js:1092
#: ../app/ui.js:1112
msgid "Connected (unencrypted) to "
msgstr "接続しました (暗号化されていません): "
#: ../app/ui.js:1115
#: ../app/ui.js:1135
msgid "Something went wrong, connection is closed"
msgstr "何らかの問題で、接続が閉じられました"
#: ../app/ui.js:1118
#: ../app/ui.js:1138
msgid "Failed to connect to server"
msgstr "サーバーへの接続に失敗しました"
#: ../app/ui.js:1128
#: ../app/ui.js:1150
msgid "Disconnected"
msgstr "切断しました"
#: ../app/ui.js:1143
#: ../app/ui.js:1165
msgid "New connection has been rejected with reason: "
msgstr "新規接続は次の理由で拒否されました: "
#: ../app/ui.js:1146
#: ../app/ui.js:1168
msgid "New connection has been rejected"
msgstr "新規接続は拒否されました"
#: ../app/ui.js:1181
#: ../app/ui.js:1234
msgid "Credentials are required"
msgstr "資格情報が必要です"
#: ../vnc.html:74
#: ../vnc.html:57
msgid "noVNC encountered an error:"
msgstr "noVNC でエラーが発生しました:"
#: ../vnc.html:84
#: ../vnc.html:67
msgid "Hide/Show the control bar"
msgstr "コントロールバーを隠す/表示する"
#: ../vnc.html:91
#: ../vnc.html:76
msgid "Drag"
msgstr "ドラッグ"
#: ../vnc.html:91
#: ../vnc.html:76
msgid "Move/Drag Viewport"
msgstr "ビューポートを移動/ドラッグ"
#: ../vnc.html:97
#: ../vnc.html:82
msgid "Keyboard"
msgstr "キーボード"
#: ../vnc.html:97
#: ../vnc.html:82
msgid "Show Keyboard"
msgstr "キーボードを表示"
#: ../vnc.html:102
#: ../vnc.html:87
msgid "Extra keys"
msgstr "追加キー"
#: ../vnc.html:102
#: ../vnc.html:87
msgid "Show Extra Keys"
msgstr "追加キーを表示"
#: ../vnc.html:107
#: ../vnc.html:92
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:107
#: ../vnc.html:92
msgid "Toggle Ctrl"
msgstr "Ctrl キーを切り替え"
msgstr "Ctrl キーをトグル"
#: ../vnc.html:110
#: ../vnc.html:95
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:110
#: ../vnc.html:95
msgid "Toggle Alt"
msgstr "Alt キーを切り替え"
msgstr "Alt キーをトグル"
#: ../vnc.html:113
#: ../vnc.html:98
msgid "Toggle Windows"
msgstr "Windows キーを切り替え"
msgstr "Windows キーをトグル"
#: ../vnc.html:113
#: ../vnc.html:98
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:116
#: ../vnc.html:101
msgid "Send Tab"
msgstr "Tab キーを送信"
#: ../vnc.html:116
#: ../vnc.html:101
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:119
#: ../vnc.html:104
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:119
#: ../vnc.html:104
msgid "Send Escape"
msgstr "Escape キーを送信"
#: ../vnc.html:122
#: ../vnc.html:107
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:122
#: ../vnc.html:107
msgid "Send Ctrl-Alt-Del"
msgstr "Ctrl-Alt-Del を送信"
#: ../vnc.html:129
#: ../vnc.html:114
msgid "Shutdown/Reboot"
msgstr "シャットダウン/再起動"
#: ../vnc.html:129
#: ../vnc.html:114
msgid "Shutdown/Reboot..."
msgstr "シャットダウン/再起動..."
#: ../vnc.html:135
#: ../vnc.html:120
msgid "Power"
msgstr "電源"
#: ../vnc.html:137
#: ../vnc.html:122
msgid "Shutdown"
msgstr "シャットダウン"
#: ../vnc.html:138
#: ../vnc.html:123
msgid "Reboot"
msgstr "再起動"
#: ../vnc.html:139
#: ../vnc.html:124
msgid "Reset"
msgstr "リセット"
#: ../vnc.html:144 ../vnc.html:150
#: ../vnc.html:129 ../vnc.html:135
msgid "Clipboard"
msgstr "クリップボード"
#: ../vnc.html:154
msgid "Clear"
msgstr "クリア"
#: ../vnc.html:137
msgid "Edit clipboard content in the textarea below."
msgstr "以下の入力欄からクリップボードの内容を編集できます。"
#: ../vnc.html:160
msgid "Fullscreen"
#: ../vnc.html:145
msgid "Full Screen"
msgstr "全画面表示"
#: ../vnc.html:165 ../vnc.html:172
#: ../vnc.html:150 ../vnc.html:156
msgid "Settings"
msgstr "設定"
#: ../vnc.html:175
#: ../vnc.html:160
msgid "Shared Mode"
msgstr "共有モード"
#: ../vnc.html:178
#: ../vnc.html:163
msgid "View Only"
msgstr "表示のみ"
msgstr "表示専用"
#: ../vnc.html:182
#: ../vnc.html:167
msgid "Clip to Window"
msgstr "ウィンドウにクリップ"
#: ../vnc.html:185
#: ../vnc.html:170
msgid "Scaling Mode:"
msgstr "スケーリングモード:"
#: ../vnc.html:187
#: ../vnc.html:172
msgid "None"
msgstr "なし"
#: ../vnc.html:188
#: ../vnc.html:173
msgid "Local Scaling"
msgstr "ローカルスケーリング"
#: ../vnc.html:189
#: ../vnc.html:174
msgid "Remote Resizing"
msgstr "リモートでリサイズ"
#: ../vnc.html:194
#: ../vnc.html:179
msgid "Advanced"
msgstr "高度"
#: ../vnc.html:197
#: ../vnc.html:182
msgid "Quality:"
msgstr "品質:"
#: ../vnc.html:201
#: ../vnc.html:186
msgid "Compression level:"
msgstr "圧縮レベル:"
#: ../vnc.html:206
#: ../vnc.html:191
msgid "Repeater ID:"
msgstr "リピーター ID:"
#: ../vnc.html:210
#: ../vnc.html:195
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:213
#: ../vnc.html:198
msgid "Encrypt"
msgstr "暗号化"
#: ../vnc.html:216
#: ../vnc.html:201
msgid "Host:"
msgstr "ホスト:"
#: ../vnc.html:220
#: ../vnc.html:205
msgid "Port:"
msgstr "ポート:"
#: ../vnc.html:224
#: ../vnc.html:209
msgid "Path:"
msgstr "パス:"
#: ../vnc.html:231
#: ../vnc.html:216
msgid "Automatic Reconnect"
msgstr "自動再接続"
#: ../vnc.html:234
#: ../vnc.html:219
msgid "Reconnect Delay (ms):"
msgstr "再接続する遅延 (ミリ秒):"
#: ../vnc.html:239
#: ../vnc.html:224
msgid "Show Dot when No Cursor"
msgstr "カーソルがないときにドットを表示"
msgstr "カーソルがないときにドットを表示する"
#: ../vnc.html:244
#: ../vnc.html:229
msgid "Logging:"
msgstr "ロギング:"
#: ../vnc.html:253
#: ../vnc.html:238
msgid "Version:"
msgstr "バージョン:"
#: ../vnc.html:261
#: ../vnc.html:246
msgid "Disconnect"
msgstr "切断"
#: ../vnc.html:280
#: ../vnc.html:269
msgid "Connect"
msgstr "接続"
#: ../vnc.html:290
#: ../vnc.html:278
msgid "Server identity"
msgstr "サーバーの識別情報"
#: ../vnc.html:281
msgid "The server has provided the following identifying information:"
msgstr "サーバーは以下の識別情報を提供しています:"
#: ../vnc.html:285
msgid "Fingerprint:"
msgstr "フィンガープリント:"
#: ../vnc.html:288
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してく"
"ださい。"
#: ../vnc.html:293
msgid "Approve"
msgstr "承認"
#: ../vnc.html:294
msgid "Reject"
msgstr "拒否"
#: ../vnc.html:302
msgid "Credentials"
msgstr "資格情報"
#: ../vnc.html:306
msgid "Username:"
msgstr "ユーザー名:"
#: ../vnc.html:294
#: ../vnc.html:310
msgid "Password:"
msgstr "パスワード:"
#: ../vnc.html:298
#: ../vnc.html:314
msgid "Send Credentials"
msgstr "資格情報を送信"
#: ../vnc.html:308
#: ../vnc.html:323
msgid "Cancel"
msgstr "キャンセル"
#~ msgid "Clear"
#~ msgstr "クリア"
#~ msgid "Password is required"
#~ msgstr "パスワードが必要です"

View File

@@ -6,9 +6,9 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.4.0\n"
"Project-Id-Version: noVNC 1.5.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2022-12-27 15:24+0100\n"
"POT-Creation-Date: 2024-06-03 14:10+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,7 +18,8 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
#: ../app/ui.js:69
msgid "HTTPS is required for full functionality"
msgid ""
"Running without HTTPS is not recommended, crashes or other issues are likely."
msgstr ""
#: ../app/ui.js:410
@@ -41,292 +42,296 @@ msgstr ""
msgid "Must set host"
msgstr ""
#: ../app/ui.js:1110
#: ../app/ui.js:1052
msgid "Failed to connect to server: "
msgstr ""
#: ../app/ui.js:1118
msgid "Connected (encrypted) to "
msgstr ""
#: ../app/ui.js:1112
#: ../app/ui.js:1120
msgid "Connected (unencrypted) to "
msgstr ""
#: ../app/ui.js:1135
#: ../app/ui.js:1143
msgid "Something went wrong, connection is closed"
msgstr ""
#: ../app/ui.js:1138
#: ../app/ui.js:1146
msgid "Failed to connect to server"
msgstr ""
#: ../app/ui.js:1150
#: ../app/ui.js:1158
msgid "Disconnected"
msgstr ""
#: ../app/ui.js:1165
#: ../app/ui.js:1173
msgid "New connection has been rejected with reason: "
msgstr ""
#: ../app/ui.js:1168
#: ../app/ui.js:1176
msgid "New connection has been rejected"
msgstr ""
#: ../app/ui.js:1234
#: ../app/ui.js:1242
msgid "Credentials are required"
msgstr ""
#: ../vnc.html:57
#: ../vnc.html:55
msgid "noVNC encountered an error:"
msgstr ""
#: ../vnc.html:67
#: ../vnc.html:65
msgid "Hide/Show the control bar"
msgstr ""
#: ../vnc.html:76
#: ../vnc.html:74
msgid "Drag"
msgstr ""
#: ../vnc.html:76
#: ../vnc.html:74
msgid "Move/Drag Viewport"
msgstr ""
#: ../vnc.html:82
#: ../vnc.html:80
msgid "Keyboard"
msgstr ""
#: ../vnc.html:82
#: ../vnc.html:80
msgid "Show Keyboard"
msgstr ""
#: ../vnc.html:87
#: ../vnc.html:85
msgid "Extra keys"
msgstr ""
#: ../vnc.html:87
#: ../vnc.html:85
msgid "Show Extra Keys"
msgstr ""
#: ../vnc.html:92
#: ../vnc.html:90
msgid "Ctrl"
msgstr ""
#: ../vnc.html:92
#: ../vnc.html:90
msgid "Toggle Ctrl"
msgstr ""
#: ../vnc.html:95
#: ../vnc.html:93
msgid "Alt"
msgstr ""
#: ../vnc.html:95
#: ../vnc.html:93
msgid "Toggle Alt"
msgstr ""
#: ../vnc.html:98
#: ../vnc.html:96
msgid "Toggle Windows"
msgstr ""
#: ../vnc.html:98
#: ../vnc.html:96
msgid "Windows"
msgstr ""
#: ../vnc.html:101
#: ../vnc.html:99
msgid "Send Tab"
msgstr ""
#: ../vnc.html:101
#: ../vnc.html:99
msgid "Tab"
msgstr ""
#: ../vnc.html:104
#: ../vnc.html:102
msgid "Esc"
msgstr ""
#: ../vnc.html:104
#: ../vnc.html:102
msgid "Send Escape"
msgstr ""
#: ../vnc.html:107
#: ../vnc.html:105
msgid "Ctrl+Alt+Del"
msgstr ""
#: ../vnc.html:107
#: ../vnc.html:105
msgid "Send Ctrl-Alt-Del"
msgstr ""
#: ../vnc.html:114
#: ../vnc.html:112
msgid "Shutdown/Reboot"
msgstr ""
#: ../vnc.html:114
#: ../vnc.html:112
msgid "Shutdown/Reboot..."
msgstr ""
#: ../vnc.html:120
#: ../vnc.html:118
msgid "Power"
msgstr ""
#: ../vnc.html:122
#: ../vnc.html:120
msgid "Shutdown"
msgstr ""
#: ../vnc.html:123
#: ../vnc.html:121
msgid "Reboot"
msgstr ""
#: ../vnc.html:124
#: ../vnc.html:122
msgid "Reset"
msgstr ""
#: ../vnc.html:129 ../vnc.html:135
#: ../vnc.html:127 ../vnc.html:133
msgid "Clipboard"
msgstr ""
#: ../vnc.html:137
#: ../vnc.html:135
msgid "Edit clipboard content in the textarea below."
msgstr ""
#: ../vnc.html:145
#: ../vnc.html:143
msgid "Full Screen"
msgstr ""
#: ../vnc.html:150 ../vnc.html:156
#: ../vnc.html:148 ../vnc.html:154
msgid "Settings"
msgstr ""
#: ../vnc.html:160
#: ../vnc.html:158
msgid "Shared Mode"
msgstr ""
#: ../vnc.html:163
#: ../vnc.html:161
msgid "View Only"
msgstr ""
#: ../vnc.html:167
#: ../vnc.html:165
msgid "Clip to Window"
msgstr ""
#: ../vnc.html:170
#: ../vnc.html:168
msgid "Scaling Mode:"
msgstr ""
#: ../vnc.html:172
#: ../vnc.html:170
msgid "None"
msgstr ""
#: ../vnc.html:173
#: ../vnc.html:171
msgid "Local Scaling"
msgstr ""
#: ../vnc.html:174
#: ../vnc.html:172
msgid "Remote Resizing"
msgstr ""
#: ../vnc.html:179
#: ../vnc.html:177
msgid "Advanced"
msgstr ""
#: ../vnc.html:182
#: ../vnc.html:180
msgid "Quality:"
msgstr ""
#: ../vnc.html:186
#: ../vnc.html:184
msgid "Compression level:"
msgstr ""
#: ../vnc.html:191
#: ../vnc.html:189
msgid "Repeater ID:"
msgstr ""
#: ../vnc.html:195
#: ../vnc.html:193
msgid "WebSocket"
msgstr ""
#: ../vnc.html:198
#: ../vnc.html:196
msgid "Encrypt"
msgstr ""
#: ../vnc.html:201
#: ../vnc.html:199
msgid "Host:"
msgstr ""
#: ../vnc.html:205
#: ../vnc.html:203
msgid "Port:"
msgstr ""
#: ../vnc.html:209
#: ../vnc.html:207
msgid "Path:"
msgstr ""
#: ../vnc.html:216
#: ../vnc.html:214
msgid "Automatic Reconnect"
msgstr ""
#: ../vnc.html:219
#: ../vnc.html:217
msgid "Reconnect Delay (ms):"
msgstr ""
#: ../vnc.html:224
#: ../vnc.html:222
msgid "Show Dot when No Cursor"
msgstr ""
#: ../vnc.html:229
#: ../vnc.html:227
msgid "Logging:"
msgstr ""
#: ../vnc.html:238
#: ../vnc.html:236
msgid "Version:"
msgstr ""
#: ../vnc.html:246
#: ../vnc.html:244
msgid "Disconnect"
msgstr ""
#: ../vnc.html:269
#: ../vnc.html:267
msgid "Connect"
msgstr ""
#: ../vnc.html:278
#: ../vnc.html:276
msgid "Server identity"
msgstr ""
#: ../vnc.html:281
#: ../vnc.html:279
msgid "The server has provided the following identifying information:"
msgstr ""
#: ../vnc.html:285
#: ../vnc.html:283
msgid "Fingerprint:"
msgstr ""
#: ../vnc.html:288
#: ../vnc.html:286
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
#: ../vnc.html:293
#: ../vnc.html:291
msgid "Approve"
msgstr ""
#: ../vnc.html:294
#: ../vnc.html:292
msgid "Reject"
msgstr ""
#: ../vnc.html:302
#: ../vnc.html:300
msgid "Credentials"
msgstr ""
#: ../vnc.html:306
#: ../vnc.html:304
msgid "Username:"
msgstr ""
#: ../vnc.html:310
#: ../vnc.html:308
msgid "Password:"
msgstr ""
#: ../vnc.html:314
#: ../vnc.html:312
msgid "Send Credentials"
msgstr ""
#: ../vnc.html:323
#: ../vnc.html:321
msgid "Cancel"
msgstr ""

View File

@@ -15,58 +15,42 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: ../app/ui.js:395
#: ../app/ui.js:430
msgid "Connecting..."
msgstr "连接中..."
#: ../app/ui.js:402
#: ../app/ui.js:438
msgid "Connected (encrypted) to "
msgstr "已连接(已加密)到"
#: ../app/ui.js:440
msgid "Connected (unencrypted) to "
msgstr "已连接(未加密)到"
#: ../app/ui.js:446
msgid "Disconnecting..."
msgstr "正在断开连接..."
#: ../app/ui.js:408
msgid "Reconnecting..."
msgstr "重新连接中..."
#: ../app/ui.js:413
msgid "Internal error"
msgstr "内部错误"
#: ../app/ui.js:1015
msgid "Must set host"
msgstr "请提供主机名"
#: ../app/ui.js:1097
msgid "Connected (encrypted) to "
msgstr "已连接到(加密)"
#: ../app/ui.js:1099
msgid "Connected (unencrypted) to "
msgstr "已连接到(未加密)"
#: ../app/ui.js:1120
msgid "Something went wrong, connection is closed"
msgstr "发生错误,连接已关闭"
#: ../app/ui.js:1123
msgid "Failed to connect to server"
msgstr "无法连接到服务器"
#: ../app/ui.js:1133
#: ../app/ui.js:450
msgid "Disconnected"
msgstr "已断开连接"
#: ../app/ui.js:1146
msgid "New connection has been rejected with reason: "
msgstr "连接被拒绝,原因:"
#: ../app/ui.js:1052 ../core/rfb.js:248
msgid "Must set host"
msgstr "必须设置主机"
#: ../app/ui.js:1149
msgid "New connection has been rejected"
msgstr "连接被拒绝"
#: ../app/ui.js:1101
msgid "Reconnecting..."
msgstr "重新连接中..."
#: ../app/ui.js:1170
#: ../app/ui.js:1140
msgid "Password is required"
msgstr "请提供密码"
#: ../core/rfb.js:548
msgid "Disconnect timeout"
msgstr "超时断开"
#: ../vnc.html:89
msgid "noVNC encountered an error:"
msgstr "noVNC 遇到一个错误:"
@@ -77,31 +61,31 @@ msgstr "显示/隐藏控制栏"
#: ../vnc.html:106
msgid "Move/Drag Viewport"
msgstr "拖放显示范围"
msgstr "移动/拖动窗口"
#: ../vnc.html:106
msgid "viewport drag"
msgstr "显示范围拖放"
msgstr "窗口拖动"
#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121
msgid "Active Mouse Button"
msgstr "启动鼠标按"
msgstr "启动鼠标按"
#: ../vnc.html:112
msgid "No mousebutton"
msgstr "禁用鼠标按"
msgstr "禁用鼠标按"
#: ../vnc.html:115
msgid "Left mousebutton"
msgstr "鼠标左"
msgstr "鼠标左"
#: ../vnc.html:118
msgid "Middle mousebutton"
msgstr "鼠标中"
msgstr "鼠标中"
#: ../vnc.html:121
msgid "Right mousebutton"
msgstr "鼠标右"
msgstr "鼠标右"
#: ../vnc.html:124
msgid "Keyboard"
@@ -127,6 +111,10 @@ msgstr "Ctrl"
msgid "Toggle Ctrl"
msgstr "切换 Ctrl"
#: ../vnc.html:136
msgid "Edit clipboard content in the textarea below."
msgstr "在下面的文本区域中编辑剪贴板内容。"
#: ../vnc.html:139
msgid "Alt"
msgstr "Alt"
@@ -153,19 +141,19 @@ msgstr "发送 Escape 键"
#: ../vnc.html:148
msgid "Ctrl+Alt+Del"
msgstr "Ctrl-Alt-Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:148
msgid "Send Ctrl-Alt-Del"
msgstr "发送 Ctrl-Alt-Del 键"
msgstr "发送 Ctrl+Alt+Del 键"
#: ../vnc.html:156
msgid "Shutdown/Reboot"
msgstr "关机/重新启动"
msgstr "关机/重"
#: ../vnc.html:156
msgid "Shutdown/Reboot..."
msgstr "关机/重新启动..."
msgstr "关机/重..."
#: ../vnc.html:162
msgid "Power"
@@ -177,7 +165,7 @@ msgstr "关机"
#: ../vnc.html:165
msgid "Reboot"
msgstr "重新启动"
msgstr "重"
#: ../vnc.html:166
msgid "Reset"
@@ -199,6 +187,10 @@ msgstr "全屏"
msgid "Settings"
msgstr "设置"
#: ../vnc.html:200
msgid "Encrypt"
msgstr "加密"
#: ../vnc.html:202
msgid "Shared Mode"
msgstr "分享模式"
@@ -224,61 +216,69 @@ msgid "Local Scaling"
msgstr "本地缩放"
#: ../vnc.html:216
msgid "Local Downscaling"
msgstr "降低本地尺寸"
#: ../vnc.html:217
msgid "Remote Resizing"
msgstr "远程调整大小"
#: ../vnc.html:221
#: ../vnc.html:222
msgid "Advanced"
msgstr "高级"
#: ../vnc.html:224
#: ../vnc.html:225
msgid "Local Cursor"
msgstr "本地光标"
#: ../vnc.html:229
msgid "Repeater ID:"
msgstr "中继站 ID"
#: ../vnc.html:228
#: ../vnc.html:233
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:231
msgid "Encrypt"
msgstr "加密"
#: ../vnc.html:234
#: ../vnc.html:239
msgid "Host:"
msgstr "主机:"
#: ../vnc.html:238
#: ../vnc.html:243
msgid "Port:"
msgstr "端口:"
#: ../vnc.html:242
#: ../vnc.html:247
msgid "Path:"
msgstr "路径:"
#: ../vnc.html:249
#: ../vnc.html:254
msgid "Automatic Reconnect"
msgstr "自动重新连接"
#: ../vnc.html:252
#: ../vnc.html:257
msgid "Reconnect Delay (ms):"
msgstr "重新连接间隔 (ms)"
#: ../vnc.html:258
#: ../vnc.html:263
msgid "Logging:"
msgstr "日志级别:"
#: ../vnc.html:270
#: ../vnc.html:275
msgid "Disconnect"
msgstr "断连接"
msgstr "断连接"
#: ../vnc.html:289
#: ../vnc.html:294
msgid "Connect"
msgstr "连接"
#: ../vnc.html:299
#: ../vnc.html:304
msgid "Password:"
msgstr "密码:"
#: ../vnc.html:313
#: ../vnc.html:318
msgid "Cancel"
msgstr "取消"
#: ../vnc.html:334
msgid "Canvas not supported."
msgstr "不支持 Canvas。"

View File

@@ -1,5 +1,5 @@
name: novnc
base: core18 # the base snap is the execution environment for this snap
base: core22 # the base snap is the execution environment for this snap
version: git
summary: Open Source VNC client using HTML5 (WebSockets, Canvas)
description: |
@@ -42,7 +42,7 @@ parts:
- jq
websockify:
source: https://github.com/novnc/websockify/archive/v0.9.0.tar.gz
source: https://github.com/novnc/websockify/archive/v0.12.0.tar.gz
plugin: python
stage-packages:
- python3-numpy

View File

@@ -1,15 +0,0 @@
{
"env": {
"node": true,
"mocha": true
},
"globals": {
"chai": false,
"sinon": false
},
"rules": {
"prefer-arrow-callback": 0,
// Too many anonymous callbacks
"func-names": "off",
}
}

View File

@@ -42,7 +42,7 @@ export default class FakeWebSocket {
}
_getSentData() {
const res = new Uint8Array(this._sendQueue.buffer, 0, this.bufferedAmount);
const res = this._sendQueue.slice(0, this.bufferedAmount);
this.bufferedAmount = 0;
return res;
}
@@ -55,11 +55,15 @@ export default class FakeWebSocket {
}
_receiveData(data) {
// Break apart the data to expose bugs where we assume data is
// neatly packaged
for (let i = 0;i < data.length;i++) {
let buf = data.subarray(i, i+1);
this.onmessage(new MessageEvent("message", { 'data': buf }));
if (data.length < 4096) {
// Break apart the data to expose bugs where we assume data is
// neatly packaged
for (let i = 0;i < data.length;i++) {
let buf = data.slice(i, i+1);
this.onmessage(new MessageEvent("message", { 'data': buf.buffer }));
}
} else {
this.onmessage(new MessageEvent("message", { 'data': data.buffer }));
}
}
}

View File

@@ -24,7 +24,7 @@ function loadFile() {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.onload = resolve;
script.onerror = reject;
script.onerror = () => { reject("Failed to load " + fname); };
document.body.appendChild(script);
script.src = "../recordings/" + fname;
});
@@ -200,6 +200,9 @@ function start() {
player.onrfbdisconnected = (evt) => {
if (!evt.detail.clean) {
message(`noVNC sent disconnected during iteration ${evt.detail.iteration} frame ${evt.detail.frame}`);
document.getElementById('startButton').disabled = false;
document.getElementById('startButton').value = "Start";
}
};
player.onfinish = (evt) => {

View File

@@ -131,12 +131,10 @@ export default class RecordingPlayer {
_doPacket() {
// Avoid having excessive queue buildup in non-realtime mode
if (this._trafficManagement && this._rfb._flushing) {
const orig = this._rfb._display.onflush;
this._rfb._display.onflush = () => {
this._rfb._display.onflush = orig;
this._rfb._onFlush();
this._doPacket();
};
this._rfb.flush()
.then(() => {
this._doPacket();
});
return;
}
@@ -150,13 +148,8 @@ export default class RecordingPlayer {
_finish() {
if (this._rfb._display.pending()) {
this._rfb._display.onflush = () => {
if (this._rfb._flushing) {
this._rfb._onFlush();
}
this._finish();
};
this._rfb._display.flush();
this._rfb._display.flush()
.then(() => { this._finish(); });
} else {
this._running = false;
this._ws.onclose({code: 1000, reason: ""});

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { isMac, isWindows, isIOS, isAndroid, isChromeOS,

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
describe('CopyRect Decoder', function () {
@@ -47,12 +50,15 @@ describe('CopyRect Decoder', function () {
display.fillRect(0, 0, 2, 2, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 0, 2, 2, 2,
[0x00, 0x02, 0x00, 0x00],
display, 24);
testDecodeRect(decoder, 2, 2, 2, 2,
[0x00, 0x00, 0x00, 0x00],
display, 24);
let done;
done = testDecodeRect(decoder, 0, 2, 2, 2,
[0x00, 0x02, 0x00, 0x00],
display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 2, 2, 2,
[0x00, 0x00, 0x00, 0x00],
display, 24);
expect(done).to.be.true;
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -69,7 +75,9 @@ describe('CopyRect Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [0x00, 0x00, 0x00, 0x00], display, 24);
let done = testDecodeRect(decoder, 1, 2, 0, 0,
[0x00, 0x00, 0x00, 0x00],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -78,6 +86,7 @@ describe('CopyRect Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
});

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { inflateInit, inflate } from "../vendor/pako/lib/zlib/inflate.js";

View File

@@ -298,14 +298,11 @@ describe('Display/Canvas Helper', function () {
expect(display).to.have.displayed(checkedData);
});
it('should support drawing images via #imageRect', function (done) {
it('should support drawing images via #imageRect', async function () {
display.imageRect(0, 0, 4, 4, "image/png", makeImagePng(checkedData, 4, 4));
display.flip();
display.onflush = () => {
expect(display).to.have.displayed(checkedData);
done();
};
display.flush();
await display.flush();
expect(display).to.have.displayed(checkedData);
});
it('should support blit images with true color via #blitImage', function () {
@@ -360,12 +357,11 @@ describe('Display/Canvas Helper', function () {
expect(img.addEventListener).to.have.been.calledOnce;
});
it('should call callback when queue is flushed', function () {
display.onflush = sinon.spy();
it('should resolve promise when queue is flushed', async function () {
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
expect(display.onflush).to.not.have.been.called;
display.flush();
expect(display.onflush).to.have.been.calledOnce;
let promise = display.flush();
expect(promise).to.be.an.instanceOf(Promise);
await promise;
});
it('should draw a blit image on type "blit"', function () {

View File

@@ -108,6 +108,8 @@ describe('Helpers', function () {
});
it('should use charCode if no key', function () {
expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('Š');
// Broken Oculus browser
expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43, key: 'Unidentified'})).to.be.equal('Š');
});
it('should return Unidentified when it cannot map the key', function () {
expect(KeyboardUtil.getKey({keycode: 0x42})).to.be.equal('Unidentified');

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
function push32(arr, num) {
@@ -62,7 +65,7 @@ describe('Hextile Decoder', function () {
data.push(2 | (2 << 4)); // x: 2, y: 2
data.push(1 | (1 << 4)); // width: 2, height: 2
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -71,6 +74,7 @@ describe('Hextile Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -92,8 +96,9 @@ describe('Hextile Decoder', function () {
data.push(0);
}
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -102,13 +107,14 @@ describe('Hextile Decoder', function () {
data.push(0x02);
push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let expected = [];
for (let i = 0; i < 16; i++) {
push32(expected, 0x00ff00ff);
}
expect(done).to.be.true;
expect(display).to.have.displayed(new Uint8Array(expected));
});
@@ -125,7 +131,7 @@ describe('Hextile Decoder', function () {
// send an empty frame
data.push(0x00);
testDecodeRect(decoder, 0, 0, 32, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 32, 4, data, display, 24);
let expected = [];
for (let i = 0; i < 16; i++) {
@@ -135,6 +141,7 @@ describe('Hextile Decoder', function () {
push32(expected, 0x00ff00ff); // rect 2: same bkground color
}
expect(done).to.be.true;
expect(display).to.have.displayed(new Uint8Array(expected));
});
@@ -156,7 +163,7 @@ describe('Hextile Decoder', function () {
data.push(2 | (2 << 4)); // x: 2, y: 2
data.push(1 | (1 << 4)); // width: 2, height: 2
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -165,6 +172,7 @@ describe('Hextile Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -190,7 +198,7 @@ describe('Hextile Decoder', function () {
data.push(0); // x: 0, y: 0
data.push(1 | (1 << 4)); // width: 2, height: 2
testDecodeRect(decoder, 0, 0, 4, 17, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 17, data, display, 24);
let targetData = [
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -205,6 +213,7 @@ describe('Hextile Decoder', function () {
}
expected = expected.concat(targetData.slice(0, 16));
expect(done).to.be.true;
expect(display).to.have.displayed(new Uint8Array(expected));
});
@@ -218,7 +227,7 @@ describe('Hextile Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);
let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -227,6 +236,7 @@ describe('Hextile Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
});

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { deflateInit, deflate, Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { toUnsigned32bit, toSigned32bit } from '../core/util/int.js';

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
describe('JPEG Decoder', function () {
@@ -41,7 +44,7 @@ describe('JPEG Decoder', function () {
display.resize(4, 4);
});
it('should handle JPEG rects', function (done) {
it('should handle JPEG rects', async function () {
let data = [
// JPEG data
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
@@ -131,7 +134,8 @@ describe('JPEG Decoder', function () {
0xff, 0xd9,
];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
expect(decodeDone).to.be.true;
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
@@ -147,14 +151,11 @@ describe('JPEG Decoder', function () {
return diff < 5;
}
display.onflush = () => {
expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
await display.flush();
expect(display).to.have.displayed(targetData, almost);
});
it('should handle JPEG rects without Huffman and quantification tables', function (done) {
it('should handle JPEG rects without Huffman and quantification tables', async function () {
let data1 = [
// JPEG data
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
@@ -244,7 +245,12 @@ describe('JPEG Decoder', function () {
0xff, 0xd9,
];
testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24);
let decodeDone;
decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24);
expect(decodeDone).to.be.true;
display.fillRect(0, 0, 4, 4, [128, 128, 128, 255]);
let data2 = [
// JPEG data
@@ -263,7 +269,8 @@ describe('JPEG Decoder', function () {
0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, 0xff, 0xd9,
];
testDecodeRect(decoder, 0, 0, 4, 4, data2, display, 24);
decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data2, display, 24);
expect(decodeDone).to.be.true;
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
@@ -279,10 +286,7 @@ describe('JPEG Decoder', function () {
return diff < 5;
}
display.onflush = () => {
expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
await display.flush();
expect(display).to.have.displayed(targetData, almost);
});
});

View File

@@ -14,6 +14,10 @@ describe('Key Event Handling', function () {
}
e.stopPropagation = sinon.spy();
e.preventDefault = sinon.spy();
e.getModifierState = function (key) {
return e[key];
};
return e;
}
@@ -310,6 +314,50 @@ describe('Key Event Handling', function () {
});
});
describe('Modifier status info', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
Object.defineProperty(window, "navigator", {value: {}});
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
});
it('should provide caps lock state', function () {
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: false, CapsLock: true}));
expect(kbd.onkeyevent).to.have.been.calledOnce;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, "KeyA", true, false, true);
});
it('should provide num lock state', function () {
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: true, CapsLock: false}));
expect(kbd.onkeyevent).to.have.been.calledOnce;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, "KeyA", true, true, false);
});
it('should have no num lock state on mac', function () {
window.navigator.platform = "Mac";
const kbd = new Keyboard(document);
kbd.onkeyevent = sinon.spy();
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'A', NumLock: false, CapsLock: true}));
expect(kbd.onkeyevent).to.have.been.calledOnce;
expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x41, "KeyA", true, null, true);
});
});
describe('Japanese IM keys on Windows', function () {
let origNavigator;
beforeEach(function () {

View File

@@ -1,61 +1,146 @@
const expect = chai.expect;
import { l10n } from '../app/localization.js';
import _, { Localizer, l10n } from '../app/localization.js';
describe('Localization', function () {
"use strict";
let origNavigator;
let fetch;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
Object.defineProperty(window, "navigator", {value: {}});
window.navigator.languages = [];
fetch = sinon.stub(window, "fetch");
fetch.resolves(new Response("{}"));
});
afterEach(function () {
fetch.restore();
Object.defineProperty(window, "navigator", origNavigator);
});
describe('Singleton', function () {
it('should export a singleton object', function () {
expect(l10n).to.be.instanceOf(Localizer);
});
it('should export a singleton translation function', async function () {
// FIXME: Can we use some spy instead?
window.navigator.languages = ["de"];
fetch.resolves(new Response(JSON.stringify({ "Foobar": "gazonk" })));
await l10n.setup(["de"]);
expect(_("Foobar")).to.equal("gazonk");
});
});
describe('language selection', function () {
let origNavigator;
beforeEach(function () {
// window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these
// tests.
origNavigator = Object.getOwnPropertyDescriptor(window, "navigator");
Object.defineProperty(window, "navigator", {value: {}});
window.navigator.languages = [];
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
});
it('should use English by default', function () {
expect(l10n.language).to.equal('en');
let lclz = new Localizer();
expect(lclz.language).to.equal('en');
});
it('should use English if no user language matches', function () {
it('should use English if no user language matches', async function () {
window.navigator.languages = ["nl", "de"];
l10n.setup(["es", "fr"]);
expect(l10n.language).to.equal('en');
let lclz = new Localizer();
await lclz.setup(["es", "fr"]);
expect(lclz.language).to.equal('en');
});
it('should use the most preferred user language', function () {
it('should fall back to generic English for other English', async function () {
window.navigator.languages = ["en-AU", "de"];
let lclz = new Localizer();
await lclz.setup(["de", "fr", "en-GB"]);
expect(lclz.language).to.equal('en');
});
it('should prefer specific English over generic', async function () {
window.navigator.languages = ["en-GB", "de"];
let lclz = new Localizer();
await lclz.setup(["de", "en-AU", "en-GB"]);
expect(lclz.language).to.equal('en-GB');
});
it('should use the most preferred user language', async function () {
window.navigator.languages = ["nl", "de", "fr"];
l10n.setup(["es", "fr", "de"]);
expect(l10n.language).to.equal('de');
let lclz = new Localizer();
await lclz.setup(["es", "fr", "de"]);
expect(lclz.language).to.equal('de');
});
it('should prefer sub-languages languages', function () {
it('should prefer sub-languages languages', async function () {
window.navigator.languages = ["pt-BR"];
l10n.setup(["pt", "pt-BR"]);
expect(l10n.language).to.equal('pt-BR');
let lclz = new Localizer();
await lclz.setup(["pt", "pt-BR"]);
expect(lclz.language).to.equal('pt-BR');
});
it('should fall back to language "parents"', function () {
it('should fall back to language "parents"', async function () {
window.navigator.languages = ["pt-BR"];
l10n.setup(["fr", "pt", "de"]);
expect(l10n.language).to.equal('pt');
let lclz = new Localizer();
await lclz.setup(["fr", "pt", "de"]);
expect(lclz.language).to.equal('pt');
});
it('should not use specific language when user asks for a generic language', function () {
it('should not use specific language when user asks for a generic language', async function () {
window.navigator.languages = ["pt", "de"];
l10n.setup(["fr", "pt-BR", "de"]);
expect(l10n.language).to.equal('de');
let lclz = new Localizer();
await lclz.setup(["fr", "pt-BR", "de"]);
expect(lclz.language).to.equal('de');
});
it('should handle underscore as a separator', function () {
it('should handle underscore as a separator', async function () {
window.navigator.languages = ["pt-BR"];
l10n.setup(["pt_BR"]);
expect(l10n.language).to.equal('pt_BR');
let lclz = new Localizer();
await lclz.setup(["pt_BR"]);
expect(lclz.language).to.equal('pt_BR');
});
it('should handle difference in case', function () {
it('should handle difference in case', async function () {
window.navigator.languages = ["pt-br"];
l10n.setup(["pt-BR"]);
expect(l10n.language).to.equal('pt-BR');
let lclz = new Localizer();
await lclz.setup(["pt-BR"]);
expect(lclz.language).to.equal('pt-BR');
});
});
describe('Translation loading', function () {
it('should not fetch a translation for English', async function () {
window.navigator.languages = [];
let lclz = new Localizer();
await lclz.setup([]);
expect(fetch).to.not.have.been.called;
});
it('should fetch dictionary relative base URL', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
let lclz = new Localizer();
await lclz.setup(["ru", "fr"], "/some/path/");
expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json");
expect(lclz.get("Foobar")).to.equal("gazonk");
});
it('should handle base URL without trailing slash', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
let lclz = new Localizer();
await lclz.setup(["ru", "fr"], "/some/path");
expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json");
expect(lclz.get("Foobar")).to.equal("gazonk");
});
it('should handle current base URL', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{ "Foobar": "gazonk" }'));
let lclz = new Localizer();
await lclz.setup(["ru", "fr"]);
expect(fetch).to.have.been.calledOnceWith("fr.json");
expect(lclz.get("Foobar")).to.equal("gazonk");
});
it('should fail if dictionary cannot be found', async function () {
window.navigator.languages = ["de", "fr"];
fetch.resolves(new Response('{}', { status: 404 }));
let lclz = new Localizer();
let ok = false;
try {
await lclz.setup(["ru", "fr"], "/some/path/");
} catch (e) {
ok = true;
}
expect(ok).to.be.true;
});
});
});

View File

@@ -1,357 +0,0 @@
const expect = chai.expect;
import RFB from '../core/rfb.js';
import FakeWebSocket from './fake.websocket.js';
function fakeGetRandomValues(arr) {
if (arr.length === 16) {
arr.set(new Uint8Array([
0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9,
0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5,
]));
} else {
arr.set(new Uint8Array([
0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95,
0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf,
0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf,
0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87,
0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e,
0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a,
0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3,
0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62,
0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69,
0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0,
0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c,
0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77,
0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7,
0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f,
0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15,
0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70,
0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64,
0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c,
0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14,
0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b,
0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a,
0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f,
0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f,
0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58,
0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc,
0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2,
0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f,
0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a,
0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81,
0x31, 0xb0, 0x69, 0xd4, 0x4e,
]));
}
}
async function fakeGeneratekey() {
let key = JSON.parse('{"alg":"RSA-OAEP-256","d":"B7QR2yI8sXjo8vQhJpX9odqqR\
6wIuPrTM1B1JJEKVeSrr7OYcc1FRJ52Vap9LIAU-ezigs9QDvWMxknB8motLnG69Wck37nt9_z4s8l\
FQp0nROA-oaR92HW34KNL1b2fEVWGI0N86h730MvTJC5O2cmKeMezIG-oNqbbfFyP8AW-WLdDlgZm1\
1-FjzhbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5WeoyX3yBtQy\
dpauW6rrgyWdtP4hDFIoZsX6w1i-UMWMMwlIB5FdnUSi26igVGADGpV_vGMP36bv-EHp0bY-Qp0gpI\
fLfgQ","dp":"Z1v5UceFfV2bhmbG19eGYb30jFxqoRBq36PKNY7IunMs1keYy0FpLbyGhtgMZ1Ymm\
c8wEzGYsCPEP-ykcun_rlyu7YxmcnyC9YQqTqLyqvO-7rUqDvk9TMfdqWFP6heADRhKZmEbmcau6_m\
2MwwK9kOkMKWvpqp8_TpJMnAH7zE","dq":"OBacRE15aY3NtCR4cvP5os3sT70JbDdDLHT3IHZM6r\
E35CYNpLDia2chm_wnMcYvKFW9zC2ajRZ15i9c_VXQzS7ZlTaQYBFyMt7kVhxMEMFsPv1crD6t3uEI\
j0LNuNYyy0jkon_LPZKQFK654CiL-L2YaNXOH4HbHP02dWeVQIE","e":"AQAB","ext":true,"ke\
y_ops":["decrypt"],"kty":"RSA","n":"m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3yThy1P_mcqr\
GDQkRiGVdcTxAk38T9PgLztmspF-6U5TAHO-gSmmW88AC9m6f1Mspps6r7zl-M_OG-TwvGzf3BDz8z\
Eg1FPbZV7whO1M4TCAZ0PqwG7qCc6nK1WiAhaKrSpzuPdL1igfNBsX7qu5wgw4ZTTGSLbVC_LfULQ5\
FADgFTRXUSaxm1F8C_Lwy6a2e4nTcXilmtN2IHUjHegzm-Tq2HizmR3ARdWJpESYIW5-AXoiqj29tD\
rqCmu2WPkB2psVp83IzZfaQNQzjNfvA8GpimkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q","p":"2Q_lN\
L7vCOBzAppYzCZo3WSh0hX-MOZyPUznks5U2TjmfdNZoL6_FJRiGyyLvwSiZFdEAAvpAyESFfFigng\
AqMLSf448nPg15VUGj533CotsEM0WpoEr1JCgqdUbgDAfJQIBcwOmegBqd7lWm7uzEnRCvouB70ybk\
JfpdprhkVE","q":"tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXGzslLt5nLmss8JqdLoDDrijjU-gjeRh\
7lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXbMq2sUZqJvYEyL74\
H2Zrt0RPAux7XQLEVgND6ROdXnMJ70O0","qi":"qfl4cXQkz4BNqa2De0-PfdU-8d1w3onnaGqx1D\
s2fHzD_SJ4cNghn2TksoT9Qo64b3pUjH9igi2pyEjomk6D12N6FG0e10u7vFKv3W5YqUOgTpYdbcWH\
dZ2qZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7Wk_0MdqQy8qvixWD4zLcY"}');
key = await window.crypto.subtle.importKey("jwk", key, {
name: "RSA-OAEP",
hash: {name: "SHA-256"}
}, true, ["decrypt"]);
return {privateKey: key};
}
const receiveData = new Uint8Array([
// server public key
0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42,
0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6,
0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f,
0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08,
0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a,
0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d,
0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40,
0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd,
0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33,
0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07,
0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7,
0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11,
0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7,
0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff,
0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca,
0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39,
0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda,
0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a,
0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07,
0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a,
0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a,
0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62,
0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f,
0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39,
0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3,
0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7,
0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d,
0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98,
0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71,
0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31,
0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f,
0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01,
0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x01,
// server random
0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb,
0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe,
0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31,
0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b,
0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe,
0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0,
0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79,
0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6,
0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66,
0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a,
0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4,
0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5,
0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11,
0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35,
0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32,
0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29,
0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4,
0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6,
0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab,
0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48,
0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15,
0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5,
0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a,
0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36,
0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11,
0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11,
0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59,
0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d,
0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95,
0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14,
0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e,
0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8,
0xdf, 0xcb,
// server hash
0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a,
0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47,
0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01,
0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e,
0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13,
// subtype
0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c,
0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7,
0x94, 0xd0, 0x19,
]);
const sendData = new Uint8Array([
// client public key
0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9,
0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8,
0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f,
0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f,
0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62,
0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4,
0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17,
0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12,
0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7,
0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce,
0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6,
0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d,
0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4,
0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06,
0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20,
0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74,
0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea,
0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64,
0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43,
0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12,
0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c,
0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a,
0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde,
0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39,
0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49,
0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3,
0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65,
0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc,
0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd,
0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0,
0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28,
0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80,
0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x00, 0x01,
// client random
0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6,
0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30,
0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c,
0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68,
0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4,
0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c,
0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b,
0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d,
0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e,
0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a,
0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a,
0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5,
0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62,
0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19,
0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23,
0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c,
0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed,
0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb,
0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b,
0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27,
0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde,
0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce,
0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2,
0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b,
0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd,
0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad,
0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e,
0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b,
0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83,
0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4,
0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71,
0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d,
0xed, 0x84,
// client hash
0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e,
0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea,
0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f,
0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84,
0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe,
// credentials
0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f,
0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82,
0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29,
0x91, 0x38,
]);
describe('RA2 handshake', function () {
let sock;
let rfb;
let sentData;
before(() => {
FakeWebSocket.replace();
sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues);
sinon.stub(window.crypto.subtle, "generateKey").callsFake(fakeGeneratekey);
});
after(() => {
FakeWebSocket.restore();
window.crypto.getRandomValues.restore();
window.crypto.subtle.generateKey.restore();
});
it('should fire the serververification event', function (done) {
sentData = new Uint8Array();
rfb = new RFB(document.createElement('div'), "ws://example.com");
sock = rfb._sock;
sock.send = (data) => {
let res = new Uint8Array(sentData.length + data.length);
res.set(sentData);
res.set(data, sentData.length);
sentData = res;
};
rfb._rfbInitState = "Security";
rfb._rfbVersion = 3.8;
sock._websocket._receiveData(new Uint8Array([1, 6]));
rfb.addEventListener("serververification", (e) => {
expect(e.detail.publickey).to.eql(receiveData.slice(0, 516));
done();
});
sock._websocket._receiveData(receiveData);
});
it('should handle approveServer and fire the credentialsrequired event', function (done) {
rfb.addEventListener("credentialsrequired", (e) => {
expect(e.detail.types).to.eql(["password"]);
done();
});
rfb.approveServer();
});
it('should match sendData after sending credentials', function (done) {
rfb.addEventListener("securityresult", (event) => {
expect(sentData.slice(1)).to.eql(sendData);
done();
});
rfb.sendCredentials({ "password": "123456" });
});
});

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
describe('Raw Decoder', function () {
@@ -42,22 +45,36 @@ describe('Raw Decoder', function () {
});
it('should handle the Raw encoding', function () {
testDecodeRect(decoder, 0, 0, 2, 2,
[0xff, 0x00, 0x00, 0, 0x00, 0xff, 0x00, 0,
0x00, 0xff, 0x00, 0, 0xff, 0x00, 0x00, 0],
display, 24);
testDecodeRect(decoder, 2, 0, 2, 2,
[0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0],
display, 24);
testDecodeRect(decoder, 0, 2, 4, 1,
[0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0],
display, 24);
testDecodeRect(decoder, 0, 3, 4, 1,
[0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0],
display, 24);
let done;
done = testDecodeRect(decoder, 0, 0, 2, 2,
[0xff, 0x00, 0x00, 0,
0x00, 0xff, 0x00, 0,
0x00, 0xff, 0x00, 0,
0xff, 0x00, 0x00, 0],
display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 0, 2, 2,
[0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0],
display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 2, 4, 1,
[0xee, 0x00, 0xff, 0,
0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0,
0xab, 0xee, 0xff, 0],
display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 3, 4, 1,
[0xee, 0x00, 0xff, 0,
0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0,
0xab, 0xee, 0xff, 0],
display, 24);
expect(done).to.be.true;
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
@@ -70,18 +87,24 @@ describe('Raw Decoder', function () {
});
it('should handle the Raw encoding in low colour mode', function () {
testDecodeRect(decoder, 0, 0, 2, 2,
[0x30, 0x30, 0x30, 0x30],
display, 8);
testDecodeRect(decoder, 2, 0, 2, 2,
[0x0c, 0x0c, 0x0c, 0x0c],
display, 8);
testDecodeRect(decoder, 0, 2, 4, 1,
[0x0c, 0x0c, 0x30, 0x30],
display, 8);
testDecodeRect(decoder, 0, 3, 4, 1,
[0x0c, 0x0c, 0x30, 0x30],
display, 8);
let done;
done = testDecodeRect(decoder, 0, 0, 2, 2,
[0x30, 0x30, 0x30, 0x30],
display, 8);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 0, 2, 2,
[0x0c, 0x0c, 0x0c, 0x0c],
display, 8);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 2, 4, 1,
[0x0c, 0x0c, 0x30, 0x30],
display, 8);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 3, 4, 1,
[0x0c, 0x0c, 0x30, 0x30],
display, 8);
expect(done).to.be.true;
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -98,7 +121,7 @@ describe('Raw Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);
let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -107,6 +130,7 @@ describe('Raw Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -115,7 +139,7 @@ describe('Raw Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [], display, 8);
let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 8);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -124,6 +148,7 @@ describe('Raw Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
function push16(arr, num) {
@@ -76,7 +79,7 @@ describe('RRE Decoder', function () {
push16(data, 2); // width: 2
push16(data, 2); // height: 2
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -85,6 +88,7 @@ describe('RRE Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -93,7 +97,10 @@ describe('RRE Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00, 0xff, 0xff, 0xff, 0xff ], display, 24);
let done = testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff ],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -102,6 +109,7 @@ describe('RRE Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
});

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
describe('Tight Decoder', function () {
@@ -42,9 +45,9 @@ describe('Tight Decoder', function () {
});
it('should handle fill rects', function () {
testDecodeRect(decoder, 0, 0, 4, 4,
[0x80, 0xff, 0x88, 0x44],
display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x80, 0xff, 0x88, 0x44],
display, 24);
let targetData = new Uint8Array([
0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,
@@ -53,21 +56,31 @@ describe('Tight Decoder', function () {
0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255,
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle uncompressed copy rects', function () {
let done;
let blueData = [ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff ];
let greenData = [ 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00 ];
testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24);
done = testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24);
expect(done).to.be.true;
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -89,7 +102,7 @@ describe('Tight Decoder', function () {
0x60, 0x82, 0x01, 0x99, 0x8d, 0x29, 0x02, 0xa6,
0x00, 0x7e, 0xbf, 0x0f, 0xf1 ];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -98,6 +111,7 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -110,7 +124,7 @@ describe('Tight Decoder', function () {
// Pixels
0x30, 0x30, 0xc0, 0xc0 ];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -119,6 +133,7 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -135,7 +150,7 @@ describe('Tight Decoder', function () {
0x78, 0x9c, 0x33, 0x30, 0x38, 0x70, 0xc0, 0x00,
0x8a, 0x01, 0x21, 0x3c, 0x05, 0xa1 ];
testDecodeRect(decoder, 0, 0, 4, 12, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 12, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -152,10 +167,12 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle uncompressed palette rects', function () {
let done;
let data1 = [
// Control bytes
0x40, 0x01,
@@ -171,8 +188,10 @@ describe('Tight Decoder', function () {
// Pixels
0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 ];
testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24);
testDecodeRect(decoder, 0, 2, 4, 2, data2, display, 24);
done = testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 2, 4, 2, data2, display, 24);
expect(done).to.be.true;
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -196,7 +215,40 @@ describe('Tight Decoder', function () {
0x62, 0x08, 0xc9, 0xc0, 0x00, 0x00, 0x00, 0x54,
0x00, 0x09 ];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle uncompressed gradient rects', function () {
let done;
let blueData = [ 0x40, 0x02, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00 ];
let greenData = [ 0x40, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00 ];
done = testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24);
expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24);
expect(done).to.be.true;
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -208,20 +260,17 @@ describe('Tight Decoder', function () {
expect(display).to.have.displayed(targetData);
});
it.skip('should handle uncompressed gradient rects', function () {
// Not implemented yet
});
it('should handle compressed gradient rects', function () {
let data = [
// Control byte
0x40, 0x02,
// Pixels (compressed)
0x18,
0x78, 0x9c, 0x62, 0x60, 0xf8, 0xcf, 0x00, 0x04,
0xff, 0x19, 0x19, 0xd0, 0x00, 0x44, 0x84, 0xf1,
0x3f, 0x9a, 0x30, 0x00, 0x00, 0x00, 0xff, 0xff ];
it.skip('should handle compressed gradient rects', function () {
// Not implemented yet
});
it('should handle empty copy rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00 ], display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -230,6 +279,25 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle empty copy rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
let done = testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00 ], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -238,10 +306,10 @@ describe('Tight Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x40, 0x01, 0x01,
0xff, 0xff, 0xff,
0xff, 0xff, 0xff ], display, 24);
let done = testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x40, 0x01, 0x01,
0xff, 0xff, 0xff,
0xff, 0xff, 0xff ], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -250,6 +318,26 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle empty gradient rects', function () {
display.fillRect(0, 0, 4, 4, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
let done = testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x40, 0x02 ], display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
@@ -258,8 +346,9 @@ describe('Tight Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x80, 0xff, 0xff, 0xff ], display, 24);
let done = testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x80, 0xff, 0xff, 0xff ],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -268,10 +357,11 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle JPEG rects', function (done) {
it('should handle JPEG rects', async function () {
let data = [
// Control bytes
0x90, 0xd6, 0x05,
@@ -369,7 +459,8 @@ describe('Tight Decoder', function () {
0x3f, 0xeb, 0xff, 0x00, 0xff, 0xd9,
];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
expect(decodeDone).to.be.true;
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -385,10 +476,7 @@ describe('Tight Decoder', function () {
return diff < 5;
}
display.onflush = () => {
expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
await display.flush();
expect(display).to.have.displayed(targetData, almost);
});
});

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
describe('TightPng Decoder', function () {
@@ -41,7 +44,7 @@ describe('TightPng Decoder', function () {
display.resize(4, 4);
});
it('should handle the TightPng encoding', function (done) {
it('should handle the TightPng encoding', async function () {
let data = [
// Control bytes
0xa0, 0xb4, 0x04,
@@ -119,7 +122,8 @@ describe('TightPng Decoder', function () {
0xae, 0x42, 0x60, 0x82,
];
testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24);
expect(decodeDone).to.be.true;
let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@@ -135,10 +139,7 @@ describe('TightPng Decoder', function () {
return diff < 30;
}
display.onflush = () => {
expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
await display.flush();
expect(display).to.have.displayed(targetData, almost);
});
});

View File

@@ -6,92 +6,64 @@ import FakeWebSocket from './fake.websocket.js';
describe('Websock', function () {
"use strict";
describe('Queue methods', function () {
let sock;
const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
describe('Receive queue methods', function () {
let sock, websock;
beforeEach(function () {
sock = new Websock();
// skip init
sock._allocateBuffers();
sock._rQ.set(RQ_TEMPLATE);
sock._rQlen = RQ_TEMPLATE.length;
});
describe('rQlen', function () {
it('should return the length of the receive queue', function () {
sock.rQi = 0;
expect(sock.rQlen).to.equal(RQ_TEMPLATE.length);
});
it("should return the proper length if we read some from the receive queue", function () {
sock.rQi = 1;
expect(sock.rQlen).to.equal(RQ_TEMPLATE.length - 1);
});
websock = new FakeWebSocket();
websock._open();
sock.attach(websock);
});
describe('rQpeek8', function () {
it('should peek at the next byte without poping it off the queue', function () {
const befLen = sock.rQlen;
const peek = sock.rQpeek8();
expect(sock.rQpeek8()).to.equal(peek);
expect(sock.rQlen).to.equal(befLen);
websock._receiveData(new Uint8Array([0xab, 0xcd]));
expect(sock.rQpeek8()).to.equal(0xab);
expect(sock.rQpeek8()).to.equal(0xab);
});
});
describe('rQshift8()', function () {
it('should pop a single byte from the receive queue', function () {
const peek = sock.rQpeek8();
const befLen = sock.rQlen;
expect(sock.rQshift8()).to.equal(peek);
expect(sock.rQlen).to.equal(befLen - 1);
websock._receiveData(new Uint8Array([0xab, 0xcd]));
expect(sock.rQshift8()).to.equal(0xab);
expect(sock.rQshift8()).to.equal(0xcd);
});
});
describe('rQshift16()', function () {
it('should pop two bytes from the receive queue and return a single number', function () {
const befLen = sock.rQlen;
const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1];
expect(sock.rQshift16()).to.equal(expected);
expect(sock.rQlen).to.equal(befLen - 2);
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(sock.rQshift16()).to.equal(0xabcd);
expect(sock.rQshift16()).to.equal(0x1234);
});
});
describe('rQshift32()', function () {
it('should pop four bytes from the receive queue and return a single number', function () {
const befLen = sock.rQlen;
const expected = (RQ_TEMPLATE[0] << 24) +
(RQ_TEMPLATE[1] << 16) +
(RQ_TEMPLATE[2] << 8) +
RQ_TEMPLATE[3];
expect(sock.rQshift32()).to.equal(expected);
expect(sock.rQlen).to.equal(befLen - 4);
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
expect(sock.rQshift32()).to.equal(0xabcd1234);
expect(sock.rQshift32()).to.equal(0x88ee1133);
});
});
describe('rQshiftStr', function () {
it('should shift the given number of bytes off of the receive queue and return a string', function () {
const befLen = sock.rQlen;
const befRQi = sock.rQi;
const shifted = sock.rQshiftStr(3);
expect(shifted).to.be.a('string');
expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3))));
expect(sock.rQlen).to.equal(befLen - 3);
});
it('should shift the entire rest of the queue off if no length is given', function () {
sock.rQshiftStr();
expect(sock.rQlen).to.equal(0);
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
expect(sock.rQshiftStr(4)).to.equal('\xab\xcd\x12\x34');
expect(sock.rQshiftStr(4)).to.equal('\x88\xee\x11\x33');
});
it('should be able to handle very large strings', function () {
const BIG_LEN = 500000;
const RQ_BIG = new Uint8Array(BIG_LEN);
const incoming = new Uint8Array(BIG_LEN);
let expected = "";
let letterCode = 'a'.charCodeAt(0);
for (let i = 0; i < BIG_LEN; i++) {
RQ_BIG[i] = letterCode;
incoming[i] = letterCode;
expected += String.fromCharCode(letterCode);
if (letterCode < 'z'.charCodeAt(0)) {
@@ -100,146 +72,282 @@ describe('Websock', function () {
letterCode = 'a'.charCodeAt(0);
}
}
sock._rQ.set(RQ_BIG);
sock._rQlen = RQ_BIG.length;
websock._receiveData(incoming);
const shifted = sock.rQshiftStr();
const shifted = sock.rQshiftStr(BIG_LEN);
expect(shifted).to.be.equal(expected);
expect(sock.rQlen).to.equal(0);
});
});
describe('rQshiftBytes', function () {
it('should shift the given number of bytes of the receive queue and return an array', function () {
const befLen = sock.rQlen;
const befRQi = sock.rQi;
const shifted = sock.rQshiftBytes(3);
expect(shifted).to.be.an.instanceof(Uint8Array);
expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3));
expect(sock.rQlen).to.equal(befLen - 3);
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0x88, 0xee, 0x11, 0x33]));
});
it('should shift the entire rest of the queue off if no length is given', function () {
sock.rQshiftBytes();
expect(sock.rQlen).to.equal(0);
it('should return a shared array if requested', function () {
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
const bytes = sock.rQshiftBytes(4, false);
expect(bytes).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(bytes.buffer.byteLength).to.not.equal(bytes.length);
});
});
describe('rQslice', function () {
beforeEach(function () {
sock.rQi = 0;
});
describe('rQpeekBytes', function () {
it('should not modify the receive queue', function () {
const befLen = sock.rQlen;
sock.rQslice(0, 2);
expect(sock.rQlen).to.equal(befLen);
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
});
it('should return an array containing the given slice of the receive queue', function () {
const sl = sock.rQslice(0, 2);
expect(sl).to.be.an.instanceof(Uint8Array);
expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2));
});
it('should use the rest of the receive queue if no end is given', function () {
const sl = sock.rQslice(1);
expect(sl).to.have.length(RQ_TEMPLATE.length - 1);
expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1));
});
it('should take the current rQi in to account', function () {
sock.rQi = 1;
expect(sock.rQslice(0, 2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2));
it('should return a shared array if requested', function () {
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
const bytes = sock.rQpeekBytes(4, false);
expect(bytes).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(bytes.buffer.byteLength).to.not.equal(bytes.length);
});
});
describe('rQwait', function () {
beforeEach(function () {
sock.rQi = 0;
websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
0x88, 0xee, 0x11, 0x33]));
});
it('should return true if there are not enough bytes in the receive queue', function () {
expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true;
expect(sock.rQwait('hi', 9)).to.be.true;
});
it('should return false if there are enough bytes in the receive queue', function () {
expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false;
expect(sock.rQwait('hi', 8)).to.be.false;
});
it('should return true and reduce rQi by "goback" if there are not enough bytes', function () {
sock.rQi = 5;
expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true;
expect(sock.rQi).to.equal(1);
expect(sock.rQshift32()).to.equal(0xabcd1234);
expect(sock.rQwait('hi', 8, 2)).to.be.true;
expect(sock.rQshift32()).to.equal(0x123488ee);
});
it('should raise an error if we try to go back more than possible', function () {
sock.rQi = 5;
expect(() => sock.rQwait('hi', RQ_TEMPLATE.length, 6)).to.throw(Error);
expect(sock.rQshift32()).to.equal(0xabcd1234);
expect(() => sock.rQwait('hi', 8, 6)).to.throw(Error);
});
it('should not reduce rQi if there are enough bytes', function () {
sock.rQi = 5;
sock.rQwait('hi', 1, 6);
expect(sock.rQi).to.equal(5);
expect(sock.rQshift32()).to.equal(0xabcd1234);
expect(sock.rQwait('hi', 4, 2)).to.be.false;
expect(sock.rQshift32()).to.equal(0x88ee1133);
});
});
});
describe('Send queue methods', function () {
let sock;
const bufferSize = 10 * 1024;
beforeEach(function () {
let websock = new FakeWebSocket();
websock._open();
sock = new Websock();
sock.attach(websock);
});
describe('sQpush8()', function () {
it('should send a single byte', function () {
sock.sQpush8(42);
sock.flush();
expect(sock).to.have.sent(new Uint8Array([42]));
});
it('should not send any data until flushing', function () {
sock.sQpush8(42);
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize;i++) {
sock.sQpush8(42);
}
let expected = [];
for (let i = 0;i < bufferSize;i++) {
expected.push(42);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpush16()', function () {
it('should send a number as two bytes', function () {
sock.sQpush16(420);
sock.flush();
expect(sock).to.have.sent(new Uint8Array([1, 164]));
});
it('should not send any data until flushing', function () {
sock.sQpush16(420);
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/2;i++) {
sock.sQpush16(420);
}
let expected = [];
for (let i = 0;i < bufferSize/2;i++) {
expected.push(1);
expected.push(164);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpush32()', function () {
it('should send a number as two bytes', function () {
sock.sQpush32(420420);
sock.flush();
expect(sock).to.have.sent(new Uint8Array([0, 6, 106, 68]));
});
it('should not send any data until flushing', function () {
sock.sQpush32(420420);
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/4;i++) {
sock.sQpush32(420420);
}
let expected = [];
for (let i = 0;i < bufferSize/4;i++) {
expected.push(0);
expected.push(6);
expected.push(106);
expected.push(68);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpushString()', function () {
it('should send a string buffer', function () {
sock.sQpushString('\x12\x34\x56\x78\x90');
sock.flush();
expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
});
it('should not send any data until flushing', function () {
sock.sQpushString('\x12\x34\x56\x78\x90');
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/5;i++) {
sock.sQpushString('\x12\x34\x56\x78\x90');
}
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
it('should implicitly split a large buffer', function () {
let str = '';
for (let i = 0;i <= bufferSize/5;i++) {
str += '\x12\x34\x56\x78\x90';
}
sock.sQpushString(str);
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpushBytes()', function () {
it('should send a byte buffer', function () {
sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
sock.flush();
expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
});
it('should not send any data until flushing', function () {
sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/5;i++) {
sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
}
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
it('should implicitly split a large buffer', function () {
let buffer = [];
for (let i = 0;i <= bufferSize/5;i++) {
buffer.push(0x12);
buffer.push(0x34);
buffer.push(0x56);
buffer.push(0x78);
buffer.push(0x90);
}
sock.sQpushBytes(new Uint8Array(buffer));
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('flush', function () {
beforeEach(function () {
sock._websocket = {
send: sinon.spy()
};
});
it('should actually send on the websocket', function () {
sock._websocket.bufferedAmount = 8;
sock._websocket.readyState = WebSocket.OPEN;
sock._sQ = new Uint8Array([1, 2, 3]);
sock._sQlen = 3;
const encoded = sock._encodeMessage();
sock.flush();
expect(sock._websocket.send).to.have.been.calledOnce;
expect(sock._websocket.send).to.have.been.calledWith(encoded);
expect(sock).to.have.sent(new Uint8Array([1, 2, 3]));
});
it('should not call send if we do not have anything queued up', function () {
sock._sQlen = 0;
sock._websocket.bufferedAmount = 8;
sock.flush();
expect(sock._websocket.send).not.to.have.been.called;
});
});
describe('send', function () {
beforeEach(function () {
sock.flush = sinon.spy();
});
it('should add to the send queue', function () {
sock.send([1, 2, 3]);
const sq = sock.sQ;
expect(new Uint8Array(sq.buffer, sock._sQlen - 3, 3)).to.array.equal(new Uint8Array([1, 2, 3]));
});
it('should call flush', function () {
sock.send([1, 2, 3]);
expect(sock.flush).to.have.been.calledOnce;
});
});
describe('sendString', function () {
beforeEach(function () {
sock.send = sinon.spy();
});
it('should call send after converting the string to an array', function () {
sock.sendString("\x01\x02\x03");
expect(sock.send).to.have.been.calledWith([1, 2, 3]);
expect(sock).to.have.sent(new Uint8Array([]));
});
});
});
@@ -450,9 +558,8 @@ describe('Websock', function () {
sock._allocateBuffers();
});
it('should support adding binary Uint8Array data to the receive queue', function () {
it('should support adding data to the receive queue', function () {
const msg = { data: new Uint8Array([1, 2, 3]) };
sock._mode = 'binary';
sock._recvMessage(msg);
expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
});
@@ -473,80 +580,49 @@ describe('Websock', function () {
expect(sock._eventHandlers.message).not.to.have.been.called;
});
it('should compact the receive queue when a message handler empties it', function () {
sock._eventHandlers.message = () => { sock.rQi = sock._rQlen; };
it('should compact the receive queue when fully read', function () {
sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]);
sock._rQlen = 6;
sock.rQi = 6;
sock._rQi = 6;
const msg = { data: new Uint8Array([1, 2, 3]).buffer };
sock._mode = 'binary';
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(0);
expect(sock.rQi).to.equal(0);
expect(sock._rQlen).to.equal(3);
expect(sock._rQi).to.equal(0);
});
it('should compact the receive queue when we reach the end of the buffer', function () {
sock._rQ = new Uint8Array(20);
sock._rQbufferSize = 20;
sock._rQlen = 20;
sock.rQi = 10;
sock._rQi = 10;
const msg = { data: new Uint8Array([1, 2]).buffer };
sock._mode = 'binary';
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(12);
expect(sock.rQi).to.equal(0);
expect(sock._rQi).to.equal(0);
});
it('should automatically resize the receive queue if the incoming message is larger than the buffer', function () {
sock._rQ = new Uint8Array(20);
sock._rQlen = 0;
sock.rQi = 0;
sock._rQi = 0;
sock._rQbufferSize = 20;
const msg = { data: new Uint8Array(30).buffer };
sock._mode = 'binary';
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(30);
expect(sock.rQi).to.equal(0);
expect(sock._rQi).to.equal(0);
expect(sock._rQ.length).to.equal(240); // keep the invariant that rQbufferSize / 8 >= rQlen
});
it('should automatically resize the receive queue if the incoming message is larger than 1/8th of the buffer and we reach the end of the buffer', function () {
sock._rQ = new Uint8Array(20);
sock._rQlen = 16;
sock.rQi = 16;
sock._rQi = 15;
sock._rQbufferSize = 20;
const msg = { data: new Uint8Array(6).buffer };
sock._mode = 'binary';
sock._recvMessage(msg);
expect(sock._rQlen).to.equal(6);
expect(sock.rQi).to.equal(0);
expect(sock._rQ.length).to.equal(48);
});
});
describe('Data encoding', function () {
before(function () { FakeWebSocket.replace(); });
after(function () { FakeWebSocket.restore(); });
describe('as binary data', function () {
let sock;
beforeEach(function () {
sock = new Websock();
sock.open('ws://', 'binary');
sock._websocket._open();
});
it('should only send the send queue up to the send queue length', function () {
sock._sQ = new Uint8Array([1, 2, 3, 4, 5]);
sock._sQlen = 3;
const res = sock._encodeMessage();
expect(res).to.array.equal(new Uint8Array([1, 2, 3]));
});
it('should properly pass the encoded data off to the actual WebSocket', function () {
sock.send([1, 2, 3]);
expect(sock._websocket._getSentData()).to.array.equal(new Uint8Array([1, 2, 3]));
});
expect(sock._rQlen).to.equal(7);
expect(sock._rQi).to.equal(0);
expect(sock._rQ.length).to.equal(56);
});
});
});

View File

@@ -8,39 +8,48 @@ describe('WebUtil', function () {
"use strict";
describe('config variables', function () {
let origState, origHref;
beforeEach(function () {
origState = history.state;
origHref = location.href;
});
afterEach(function () {
history.replaceState(origState, '', origHref);
});
it('should parse query string variables', function () {
// history.pushState() will not cause the browser to attempt loading
// the URL, this is exactly what we want here for the tests.
history.pushState({}, '', "test?myvar=myval");
history.replaceState({}, '', "test?myvar=myval");
expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
});
it('should return default value when no query match', function () {
history.pushState({}, '', "test?myvar=myval");
history.replaceState({}, '', "test?myvar=myval");
expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
});
it('should handle no query match and no default value', function () {
history.pushState({}, '', "test?myvar=myval");
history.replaceState({}, '', "test?myvar=myval");
expect(WebUtil.getConfigVar("other")).to.be.equal(null);
});
it('should parse fragment variables', function () {
history.pushState({}, '', "test#myvar=myval");
history.replaceState({}, '', "test#myvar=myval");
expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
});
it('should return default value when no fragment match', function () {
history.pushState({}, '', "test#myvar=myval");
history.replaceState({}, '', "test#myvar=myval");
expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
});
it('should handle no fragment match and no default value', function () {
history.pushState({}, '', "test#myvar=myval");
history.replaceState({}, '', "test#myvar=myval");
expect(WebUtil.getConfigVar("other")).to.be.equal(null);
});
it('should handle both query and fragment', function () {
history.pushState({}, '', "test?myquery=1#myhash=2");
history.replaceState({}, '', "test?myquery=1#myhash=2");
expect(WebUtil.getConfigVar("myquery")).to.be.equal("1");
expect(WebUtil.getConfigVar("myhash")).to.be.equal("2");
});
it('should prioritize fragment if both provide same var', function () {
history.pushState({}, '', "test?myvar=1#myvar=2");
history.replaceState({}, '', "test?myvar=1#myvar=2");
expect(WebUtil.getConfigVar("myvar")).to.be.equal("2");
});
});
@@ -83,6 +92,11 @@ describe('WebUtil', function () {
expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value');
expect(WebUtil.readSetting('test')).to.equal('value');
});
it('should not crash when local storage save fails', function () {
localStorage.setItem.throws(new DOMException());
expect(WebUtil.writeSetting('test', 'value')).to.not.throw;
});
});
describe('setSetting', function () {
@@ -128,6 +142,11 @@ describe('WebUtil', function () {
WebUtil.writeSetting('test', 'something else');
expect(WebUtil.readSetting('test')).to.equal('something else');
});
it('should not crash when local storage read fails', function () {
localStorage.getItem.throws(new DOMException());
expect(WebUtil.readSetting('test')).to.not.throw;
});
});
// this doesn't appear to be used anywhere
@@ -136,6 +155,11 @@ describe('WebUtil', function () {
WebUtil.eraseSetting('test');
expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test');
});
it('should not crash when local storage remove fails', function () {
localStorage.removeItem.throws(new DOMException());
expect(WebUtil.eraseSetting('test')).to.not.throw;
});
});
});

View File

@@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth);
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
describe('ZRLE Decoder', function () {
@@ -42,9 +45,11 @@ describe('ZRLE Decoder', function () {
});
it('should handle the Raw subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e, 0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12, 0x02, 0x00, 0x00, 0x00, 0xff, 0xff],
display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e,
0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12,
0x02, 0x00, 0x00, 0x00, 0xff, 0xff],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
@@ -53,13 +58,16 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle the Solid subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff],
display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e,
0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00,
0x00, 0x00, 0xff, 0xff],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
@@ -68,14 +76,18 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle the Palette Tile subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x12, 0x78, 0x5E, 0x62, 0x62, 0x60, 248, 0xff, 0x9F, 0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff],
display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x12, 0x78, 0x5E,
0x62, 0x62, 0x60, 248, 0xff, 0x9F,
0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
@@ -84,13 +96,16 @@ describe('ZRLE Decoder', function () {
0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle the RLE Tile subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e, 0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff],
display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e,
0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00,
0x00, 0x00, 0x00, 0xff, 0xff],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
@@ -99,13 +114,17 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});
it('should handle the RLE Palette Tile subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x11, 0x78, 0x5e, 0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f, 0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00, 0x00, 0xff, 0xff],
display, 24);
let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x11, 0x78, 0x5e,
0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f,
0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00,
0x00, 0xff, 0xff],
display, 24);
let targetData = new Uint8Array([
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
@@ -114,6 +133,7 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff
]);
expect(done).to.be.true;
expect(display).to.have.displayed(targetData);
});

View File

@@ -1,8 +0,0 @@
{
"env": {
"node": true
},
"rules": {
"no-console": 0
}
}

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
const path = require('path');
const program = require('commander');
const { program } = require('commander');
const fs = require('fs');
const fse = require('fs-extra');
const babel = require('@babel/core');

View File

@@ -8,13 +8,13 @@ usage() {
echo "$*"
echo
fi
echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]"
echo "Usage: ${NAME} [--listen [HOST:]PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]"
echo
echo "Starts the WebSockets proxy and a mini-webserver and "
echo "provides a cut-and-paste URL to go to."
echo
echo " --listen PORT Port for proxy/webserver to listen on"
echo " Default: 6080"
echo " --listen [HOST:]PORT Port for proxy/webserver to listen on"
echo " Default: 6080 (on all interfaces)"
echo " --vnc VNC_HOST:PORT VNC server host:port proxy target"
echo " Default: localhost:5900"
echo " --cert CERT Path to combined cert/key file, or just"
@@ -47,14 +47,16 @@ usage() {
NAME="$(basename $0)"
REAL_NAME="$(readlink -f $0)"
HERE="$(cd "$(dirname "$REAL_NAME")" && pwd)"
HOST=""
PORT="6080"
LISTEN="$PORT"
VNC_DEST="localhost:5900"
CERT=""
KEY=""
WEB=""
proxy_pid=""
SSLONLY=""
RECORD_ARG=""
RECORD=""
SYSLOG_ARG=""
HEARTBEAT_ARG=""
IDLETIMEOUT_ARG=""
@@ -86,14 +88,14 @@ cleanup() {
while [ "$*" ]; do
param=$1; shift; OPTARG=$1
case $param in
--listen) PORT="${OPTARG}"; shift ;;
--listen) LISTEN="${OPTARG}"; shift ;;
--vnc) VNC_DEST="${OPTARG}"; shift ;;
--cert) CERT="${OPTARG}"; shift ;;
--key) KEY="${OPTARG}"; shift ;;
--web) WEB="${OPTARG}"; shift ;;
--ssl-only) SSLONLY="--ssl-only" ;;
--file-only) FILEONLY_ARG="--file-only" ;;
--record) RECORD_ARG="--record ${OPTARG}"; shift ;;
--record) RECORD="${OPTARG}"; shift ;;
--syslog) SYSLOG_ARG="--syslog ${OPTARG}"; shift ;;
--heartbeat) HEARTBEAT_ARG="--heartbeat ${OPTARG}"; shift ;;
--idle-timeout) IDLETIMEOUT_ARG="--idle-timeout ${OPTARG}"; shift ;;
@@ -107,14 +109,23 @@ while [ "$*" ]; do
esac
done
if [ "$LISTEN" != "$PORT" ]; then
HOST=${LISTEN%:*}
PORT=${LISTEN##*:}
# if no host was given, restore
[ "$HOST" = "$PORT" ] && HOST=""
fi
# Sanity checks
if bash -c "exec 7<>/dev/tcp/localhost/${PORT}" &> /dev/null; then
exec 7<&-
exec 7>&-
die "Port ${PORT} in use. Try --listen PORT"
else
exec 7<&-
exec 7>&-
if [ -z "${HOST}" ]; then
if bash -c "exec 7<>/dev/tcp/localhost/${PORT}" &> /dev/null; then
exec 7<&-
exec 7>&-
die "Port ${PORT} in use. Try --listen PORT"
else
exec 7<&-
exec 7>&-
fi
fi
trap "cleanup" TERM QUIT INT EXIT
@@ -191,9 +202,14 @@ else
fi
fi
echo "Starting webserver and WebSockets proxy on port ${PORT}"
#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} &
${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${PORT} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD_ARG} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} &
# Make all file paths absolute as websockify changes working directory
WEB=`realpath "${WEB}"`
[ -n "${CERT}" ] && CERT=`realpath "${CERT}"`
[ -n "${KEY}" ] && KEY=`realpath "${KEY}"`
[ -n "${RECORD}" ] && RECORD=`realpath "${RECORD}"`
echo "Starting webserver and WebSockets proxy on${HOST:+ host ${HOST}} port ${PORT}"
${WEBSOCKIFY} ${SYSLOG_ARG} ${SSLONLY} ${FILEONLY_ARG} --web ${WEB} ${CERT:+--cert ${CERT}} ${KEY:+--key ${KEY}} ${LISTEN} ${VNC_DEST} ${HEARTBEAT_ARG} ${IDLETIMEOUT_ARG} ${RECORD:+--record ${RECORD}} ${TIMEOUT_ARG} ${WEBAUTH_ARG} ${AUTHPLUGIN_ARG} ${AUTHSOURCE_ARG} &
proxy_pid="$!"
sleep 1
if [ -z "$proxy_pid" ] || ! ps -eo pid= | grep -w "$proxy_pid" > /dev/null; then
@@ -202,11 +218,15 @@ if [ -z "$proxy_pid" ] || ! ps -eo pid= | grep -w "$proxy_pid" > /dev/null; then
exit 1
fi
if [ -z "$HOST" ]; then
HOST=$(hostname)
fi
echo -e "\n\nNavigate to this URL:\n"
if [ "x$SSLONLY" == "x" ]; then
echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n"
echo -e " http://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
else
echo -e " https://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n"
echo -e " https://${HOST}:${PORT}/vnc.html?host=${HOST}&port=${PORT}\n"
fi
echo -e "Press Ctrl-C to exit\n\n"

View File

@@ -107,20 +107,13 @@
// query string. If the variable isn't defined in the URL
// it returns the default value instead.
function readQueryVariable(name, defaultValue) {
// A URL with a query parameter can look like this (But will most probably get logged on the http server):
// A URL with a query parameter can look like this:
// https://www.example.com?myqueryparam=myvalue
//
// For privacy (Using a hastag #, the parameters will not be sent to the server)
// the url can be requested in the following way:
// https://www.example.com#myqueryparam=myvalue&password=secreatvalue
//
// Even Mixing public and non public parameters will work:
// https://www.example.com?nonsecretparam=example.com#password=secreatvalue
//
// Note that we use location.href instead of location.search
// because Firefox < 53 has a bug w.r.t location.search
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = ''.concat(document.location.href, window.location.hash).match(re);
match = document.location.href.match(re);
if (match) {
// We have to decode the URL since want the cleartext value