90 Commits

Author SHA1 Message Date
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
Pierre Ossman 90455eef06 noVNC 1.4.0 2023-01-20 13:58:48 +01:00
Pierre Ossman 51677f5c70 Update json files for new translations 2023-01-20 13:57:20 +01:00
Pierre Ossman 823e7cfca3 Update Swedish translation 2023-01-20 13:56:16 +01:00
pdlan f974b73137 Cleanup for the cryptographic algorithms that are not supported by SubtleCrypto 2023-01-20 05:54:00 -05:00
Samuel Mannehed 5b7d2a622e Fix positioning of checkbox checkmark
Changing the ::after element to be displayed as 'block' lets it be
positioned using relative. This means we can remove the confusing
"position: relative" from the checkbox.
2023-01-02 14:46:33 +01:00
Pierre Ossman 3553a451d8 Remove redundant meta charset
UTF-8 is the default for HTML5 pages anyway.
2022-12-29 13:26:41 +01:00
54 changed files with 3512 additions and 2285 deletions
+27 -21
View File
@@ -1,21 +1,22 @@
{
"HTTPS is required for full functionality": "",
"Connecting...": "En cours de connexion...",
"Disconnecting...": "Déconnexion en cours...",
"Reconnecting...": "Reconnexion en cours...",
"Internal error": "Erreur interne",
"Must set host": "Doit définir l'hôte",
"Connected (encrypted) to ": "Connecté (crypté) à ",
"Connected (unencrypted) to ": "Connecté (non crypté) à ",
"Something went wrong, connection is closed": "Quelque chose est arrivé, la connexion est fermée",
"Connected (encrypted) to ": "Connecté (chiffré) à ",
"Connected (unencrypted) to ": "Connecté (non chiffré) à ",
"Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée",
"Failed to connect to server": "Échec de connexion au serveur",
"Disconnected": "Déconnecté",
"New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec raison: ",
"New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec motif : ",
"New connection has been rejected": "Une nouvelle connexion a été rejetée",
"Credentials are required": "Les identifiants sont requis",
"noVNC encountered an error:": "noVNC a rencontré une erreur:",
"noVNC encountered an error:": "noVNC a rencontré une erreur :",
"Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
"Drag": "Faire glisser",
"Move/Drag Viewport": "Déplacer/faire glisser Viewport",
"Move/Drag Viewport": "Déplacer/faire glisser le Viewport",
"Keyboard": "Clavier",
"Show Keyboard": "Afficher le clavier",
"Extra keys": "Touches supplémentaires",
@@ -39,34 +40,39 @@
"Reboot": "Redémarrer",
"Reset": "Réinitialiser",
"Clipboard": "Presse-papiers",
"Clear": "Effacer",
"Fullscreen": "Plein écran",
"Edit clipboard content in the textarea below.": "",
"Settings": "Paramètres",
"Shared Mode": "Mode partagé",
"View Only": "Afficher uniquement",
"Clip to Window": "Clip à fenêtre",
"Scaling Mode:": "Mode mise à l'échelle:",
"Scaling Mode:": "Mode mise à l'échelle :",
"None": "Aucun",
"Local Scaling": "Mise à l'échelle locale",
"Remote Resizing": "Redimensionnement à distance",
"Advanced": "Avancé",
"Quality:": "Qualité:",
"Compression level:": "Niveau de compression:",
"Repeater ID:": "ID Répéteur:",
"Quality:": "Qualité :",
"Compression level:": "Niveau de compression :",
"Repeater ID:": "ID Répéteur :",
"WebSocket": "WebSocket",
"Encrypt": "Crypter",
"Host:": "Hôte:",
"Port:": "Port:",
"Path:": "Chemin:",
"Encrypt": "Chiffrer",
"Host:": "Hôte :",
"Port:": "Port :",
"Path:": "Chemin :",
"Automatic Reconnect": "Reconnecter automatiquemen",
"Reconnect Delay (ms):": "Délai de reconnexion (ms):",
"Reconnect Delay (ms):": "Délai de reconnexion (ms) :",
"Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
"Logging:": "Se connecter:",
"Version:": "Version:",
"Logging:": "Se connecter :",
"Version:": "Version :",
"Disconnect": "Déconnecter",
"Connect": "Connecter",
"Username:": "Nom d'utilisateur:",
"Password:": "Mot de passe:",
"Server identity": "",
"The server has provided the following identifying information:": "",
"Fingerprint:": "",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "",
"Approve": "",
"Reject": "",
"Username:": "Nom d'utilisateur :",
"Password:": "Mot de passe :",
"Send Credentials": "Envoyer les identifiants",
"Cancel": "Annuler"
}
+10 -2
View File
@@ -1,4 +1,5 @@
{
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
"Connecting...": "Ansluter...",
"Disconnecting...": "Kopplar ner...",
"Reconnecting...": "Återansluter...",
@@ -39,8 +40,8 @@
"Reboot": "Boota om",
"Reset": "Återställ",
"Clipboard": "Urklipp",
"Clear": "Rensa",
"Fullscreen": "Fullskärm",
"Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
"Full Screen": "Fullskärm",
"Settings": "Inställningar",
"Shared Mode": "Delat Läge",
"View Only": "Endast Visning",
@@ -65,6 +66,13 @@
"Version:": "Version:",
"Disconnect": "Koppla från",
"Connect": "Anslut",
"Server identity": "Server-identitet",
"The server has provided the following identifying information:": "Servern har gett följande identifierande information:",
"Fingerprint:": "Fingeravtryck:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".",
"Approve": "Godkänn",
"Reject": "Neka",
"Credentials": "Användaruppgifter",
"Username:": "Användarnamn:",
"Password:": "Lösenord:",
"Send Credentials": "Skicka Användaruppgifter",
+38 -11
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;
}
+2 -2
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{
+6 -5
View File
@@ -86,13 +86,16 @@ option {
* Checkboxes
*/
input[type=checkbox] {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
background-color: white;
background-image: unset;
border: 1px solid dimgrey;
border-radius: 3px;
width: 13px;
height: 13px;
padding: 0;
margin-right: 6px;
vertical-align: bottom;
transition: 0.2s background-color linear;
@@ -103,14 +106,12 @@ input[type=checkbox]:checked {
}
input[type=checkbox]:checked::after {
content: "";
position: absolute;
top: 0;
left: 3px;
display: block; /* width & height doesn't work on inline elements */
width: 3px;
height: 7px;
border: 1px solid white;
border-width: 0 2px 2px 0;
transform: rotate(40deg);
transform: rotate(40deg) translateY(-1px);
}
/*
+15 -19
View File
@@ -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;
+73 -9
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
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
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
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;
+68 -4
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
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);
}
}
+8 -5
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
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 };
}
}
+12 -22
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--;
}
+118 -113
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;
}
}
+25 -32
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;
+3 -7
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;
}
@@ -316,7 +312,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;
+1 -1
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);
+2 -3
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) {
+13 -10
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;
}
}
}
+1
View File
@@ -22,6 +22,7 @@ export const encodings = {
pseudoEncodingLastRect: -224,
pseudoEncodingCursor: -239,
pseudoEncodingQEMUExtendedKeyEvent: -258,
pseudoEncodingQEMULedEvent: -261,
pseudoEncodingDesktopName: -307,
pseudoEncodingExtendedDesktopSize: -308,
pseudoEncodingXvp: -309,
+1 -2
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) {
+21 -13
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,8 @@ 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 +86,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 +115,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 +126,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 +165,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 +176,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 +190,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 +207,7 @@ export default class Keyboard {
return;
}
this._sendKeyEvent(keysym, code, true);
this._sendKeyEvent(keysym, code, true, numlock, capslock);
}
_handleKeyUp(e) {
+46 -301
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;
}
}
}
+245 -369
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;
@@ -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();
}
};
+70 -58
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");
}
-6
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
+3 -3
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
+1 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@novnc/novnc",
"version": "1.4.0-beta",
"version": "1.4.0",
"description": "An HTML5 VNC client",
"browser": "lib/rfb",
"directories": {
@@ -39,17 +39,11 @@
"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",
"jsdom": "latest",
+119 -80
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 "パスワードが必要です"
+114 -75
View File
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.3.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2021-08-27 16:03+0200\n"
"PO-Revision-Date: 2021-08-27 16:18+0200\n"
"POT-Creation-Date: 2023-01-20 12:54+0100\n"
"PO-Revision-Date: 2023-01-20 12:58+0100\n"
"Last-Translator: Samuel Mannehed <samuel@cendio.se>\n"
"Language-Team: none\n"
"Language: sv\n"
@@ -17,265 +17,269 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.0.3\n"
"X-Generator: Poedit 3.2.2\n"
#: ../app/ui.js:400
#: ../app/ui.js:69
msgid "HTTPS is required for full functionality"
msgstr "HTTPS krävs för full funktionalitet"
#: ../app/ui.js:410
msgid "Connecting..."
msgstr "Ansluter..."
#: ../app/ui.js:407
#: ../app/ui.js:417
msgid "Disconnecting..."
msgstr "Kopplar ner..."
#: ../app/ui.js:413
#: ../app/ui.js:423
msgid "Reconnecting..."
msgstr "Återansluter..."
#: ../app/ui.js:418
#: ../app/ui.js:428
msgid "Internal error"
msgstr "Internt fel"
#: ../app/ui.js:1009
#: ../app/ui.js:1026
msgid "Must set host"
msgstr "Du måste specifiera en värd"
#: ../app/ui.js:1091
#: ../app/ui.js:1110
msgid "Connected (encrypted) to "
msgstr "Ansluten (krypterat) till "
#: ../app/ui.js:1093
#: ../app/ui.js:1112
msgid "Connected (unencrypted) to "
msgstr "Ansluten (okrypterat) till "
#: ../app/ui.js:1116
#: ../app/ui.js:1135
msgid "Something went wrong, connection is closed"
msgstr "Något gick fel, anslutningen avslutades"
#: ../app/ui.js:1119
#: ../app/ui.js:1138
msgid "Failed to connect to server"
msgstr "Misslyckades att ansluta till servern"
#: ../app/ui.js:1129
#: ../app/ui.js:1150
msgid "Disconnected"
msgstr "Frånkopplad"
#: ../app/ui.js:1144
#: ../app/ui.js:1165
msgid "New connection has been rejected with reason: "
msgstr "Ny anslutning har blivit nekad med följande skäl: "
#: ../app/ui.js:1147
#: ../app/ui.js:1168
msgid "New connection has been rejected"
msgstr "Ny anslutning har blivit nekad"
#: ../app/ui.js:1182
#: ../app/ui.js:1234
msgid "Credentials are required"
msgstr "Användaruppgifter krävs"
#: ../vnc.html:61
#: ../vnc.html:55
msgid "noVNC encountered an error:"
msgstr "noVNC stötte på ett problem:"
#: ../vnc.html:71
#: ../vnc.html:65
msgid "Hide/Show the control bar"
msgstr "Göm/Visa kontrollbaren"
#: ../vnc.html:78
#: ../vnc.html:74
msgid "Drag"
msgstr "Dra"
#: ../vnc.html:78
#: ../vnc.html:74
msgid "Move/Drag Viewport"
msgstr "Flytta/Dra Vyn"
#: ../vnc.html:84
#: ../vnc.html:80
msgid "Keyboard"
msgstr "Tangentbord"
#: ../vnc.html:84
#: ../vnc.html:80
msgid "Show Keyboard"
msgstr "Visa Tangentbord"
#: ../vnc.html:89
#: ../vnc.html:85
msgid "Extra keys"
msgstr "Extraknappar"
#: ../vnc.html:89
#: ../vnc.html:85
msgid "Show Extra Keys"
msgstr "Visa Extraknappar"
#: ../vnc.html:94
#: ../vnc.html:90
msgid "Ctrl"
msgstr "Ctrl"
#: ../vnc.html:94
#: ../vnc.html:90
msgid "Toggle Ctrl"
msgstr "Växla Ctrl"
#: ../vnc.html:97
#: ../vnc.html:93
msgid "Alt"
msgstr "Alt"
#: ../vnc.html:97
#: ../vnc.html:93
msgid "Toggle Alt"
msgstr "Växla Alt"
#: ../vnc.html:100
#: ../vnc.html:96
msgid "Toggle Windows"
msgstr "Växla Windows"
#: ../vnc.html:100
#: ../vnc.html:96
msgid "Windows"
msgstr "Windows"
#: ../vnc.html:103
#: ../vnc.html:99
msgid "Send Tab"
msgstr "Skicka Tab"
#: ../vnc.html:103
#: ../vnc.html:99
msgid "Tab"
msgstr "Tab"
#: ../vnc.html:106
#: ../vnc.html:102
msgid "Esc"
msgstr "Esc"
#: ../vnc.html:106
#: ../vnc.html:102
msgid "Send Escape"
msgstr "Skicka Escape"
#: ../vnc.html:109
#: ../vnc.html:105
msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del"
#: ../vnc.html:109
#: ../vnc.html:105
msgid "Send Ctrl-Alt-Del"
msgstr "Skicka Ctrl-Alt-Del"
#: ../vnc.html:116
#: ../vnc.html:112
msgid "Shutdown/Reboot"
msgstr "Stäng av/Boota om"
#: ../vnc.html:116
#: ../vnc.html:112
msgid "Shutdown/Reboot..."
msgstr "Stäng av/Boota om..."
#: ../vnc.html:122
#: ../vnc.html:118
msgid "Power"
msgstr "Ström"
#: ../vnc.html:124
#: ../vnc.html:120
msgid "Shutdown"
msgstr "Stäng av"
#: ../vnc.html:125
#: ../vnc.html:121
msgid "Reboot"
msgstr "Boota om"
#: ../vnc.html:126
#: ../vnc.html:122
msgid "Reset"
msgstr "Återställ"
#: ../vnc.html:131 ../vnc.html:137
#: ../vnc.html:127 ../vnc.html:133
msgid "Clipboard"
msgstr "Urklipp"
#: ../vnc.html:141
msgid "Clear"
msgstr "Rensa"
#: ../vnc.html:135
msgid "Edit clipboard content in the textarea below."
msgstr "Redigera urklippets innehåll i fältet nedan."
#: ../vnc.html:147
msgid "Fullscreen"
#: ../vnc.html:143
msgid "Full Screen"
msgstr "Fullskärm"
#: ../vnc.html:152 ../vnc.html:159
#: ../vnc.html:148 ../vnc.html:154
msgid "Settings"
msgstr "Inställningar"
#: ../vnc.html:162
#: ../vnc.html:158
msgid "Shared Mode"
msgstr "Delat Läge"
#: ../vnc.html:165
#: ../vnc.html:161
msgid "View Only"
msgstr "Endast Visning"
#: ../vnc.html:169
#: ../vnc.html:165
msgid "Clip to Window"
msgstr "Begränsa till Fönster"
#: ../vnc.html:172
#: ../vnc.html:168
msgid "Scaling Mode:"
msgstr "Skalningsläge:"
#: ../vnc.html:174
#: ../vnc.html:170
msgid "None"
msgstr "Ingen"
#: ../vnc.html:175
#: ../vnc.html:171
msgid "Local Scaling"
msgstr "Lokal Skalning"
#: ../vnc.html:176
#: ../vnc.html:172
msgid "Remote Resizing"
msgstr "Ändra Storlek"
#: ../vnc.html:181
#: ../vnc.html:177
msgid "Advanced"
msgstr "Avancerat"
#: ../vnc.html:184
#: ../vnc.html:180
msgid "Quality:"
msgstr "Kvalitet:"
#: ../vnc.html:188
#: ../vnc.html:184
msgid "Compression level:"
msgstr "Kompressionsnivå:"
#: ../vnc.html:193
#: ../vnc.html:189
msgid "Repeater ID:"
msgstr "Repeater-ID:"
#: ../vnc.html:197
#: ../vnc.html:193
msgid "WebSocket"
msgstr "WebSocket"
#: ../vnc.html:200
#: ../vnc.html:196
msgid "Encrypt"
msgstr "Kryptera"
#: ../vnc.html:203
#: ../vnc.html:199
msgid "Host:"
msgstr "Värd:"
#: ../vnc.html:207
#: ../vnc.html:203
msgid "Port:"
msgstr "Port:"
#: ../vnc.html:211
#: ../vnc.html:207
msgid "Path:"
msgstr "Sökväg:"
#: ../vnc.html:218
#: ../vnc.html:214
msgid "Automatic Reconnect"
msgstr "Automatisk Återanslutning"
#: ../vnc.html:221
#: ../vnc.html:217
msgid "Reconnect Delay (ms):"
msgstr "Fördröjning (ms):"
#: ../vnc.html:226
#: ../vnc.html:222
msgid "Show Dot when No Cursor"
msgstr "Visa prick när ingen muspekare finns"
#: ../vnc.html:231
#: ../vnc.html:227
msgid "Logging:"
msgstr "Loggning:"
#: ../vnc.html:240
#: ../vnc.html:236
msgid "Version:"
msgstr "Version:"
#: ../vnc.html:248
#: ../vnc.html:244
msgid "Disconnect"
msgstr "Koppla från"
@@ -283,18 +287,53 @@ msgstr "Koppla från"
msgid "Connect"
msgstr "Anslut"
#: ../vnc.html:277
#: ../vnc.html:276
msgid "Server identity"
msgstr "Server-identitet"
#: ../vnc.html:279
msgid "The server has provided the following identifying information:"
msgstr "Servern har gett följande identifierande information:"
#: ../vnc.html:283
msgid "Fingerprint:"
msgstr "Fingeravtryck:"
#: ../vnc.html:286
msgid ""
"Please verify that the information is correct and press \"Approve\". "
"Otherwise press \"Reject\"."
msgstr ""
"Kontrollera att informationen är korrekt och tryck sedan "
"\"Godkänn\". Tryck annars \"Neka\"."
#: ../vnc.html:291
msgid "Approve"
msgstr "Godkänn"
#: ../vnc.html:292
msgid "Reject"
msgstr "Neka"
#: ../vnc.html:300
msgid "Credentials"
msgstr "Användaruppgifter"
#: ../vnc.html:304
msgid "Username:"
msgstr "Användarnamn:"
#: ../vnc.html:281
#: ../vnc.html:308
msgid "Password:"
msgstr "Lösenord:"
#: ../vnc.html:285
#: ../vnc.html:312
msgid "Send Credentials"
msgstr "Skicka Användaruppgifter"
#: ../vnc.html:295
#: ../vnc.html:321
msgid "Cancel"
msgstr "Avbryt"
#~ msgid "Clear"
#~ msgstr "Rensa"
+2 -2
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.11.0.tar.gz
plugin: python
stage-packages:
- python3-numpy
+10 -6
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 }));
}
}
}
+4 -1
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) => {
+6 -13
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: ""});
+18 -9
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);
});
});
+7 -11
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 () {
+19 -9
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);
});
});
+21 -17
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);
});
});
+48
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 () {
+122 -37
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;
});
});
});
-357
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" });
});
});
+57 -32
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);
});
});
+1220 -324
View File
File diff suppressed because it is too large Load Diff
+12 -4
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);
});
});
+55 -33
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,7 @@ 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,
@@ -205,6 +224,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);
});
@@ -221,7 +241,7 @@ 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, [ 0x00 ], display, 24);
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,
@@ -230,6 +250,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);
});
@@ -238,10 +259,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 +271,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);
});
@@ -258,8 +280,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 +291,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 +393,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 +410,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);
});
});
+10 -9
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);
});
});
+261 -185
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);
});
});
});
+32 -8
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;
});
});
});
+37 -17
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);
});
+38 -18
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"
-2
View File
@@ -15,8 +15,6 @@
-->
<title>noVNC</title>
<meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico">
<!-- Apple iOS Safari settings -->
+2 -11
View File
@@ -16,8 +16,6 @@
-->
<title>noVNC</title>
<meta charset="utf-8">
<style>
body {
@@ -109,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