clean
Grégory Lebreton 8 months ago
parent 32ff8686f4
commit 24e55d946f

@ -1,73 +0,0 @@
/*
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@charset "utf-8";
#app {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
position: relative;
min-width: 320px;
}
body.app-error {
background: #b44;
color: #fff;
}
#app-loading {
width: 100%;
min-height: 100%;
display: flex;
flex-direction: column;
justify-items: center;
justify-content: center;
}
#app-loading-frame {
flex: 0 0;
padding: 30px;
text-align: center;
}
#app-loading-icon {
background: url("./widgets/busy.svg") center center no-repeat;
background-size: contain;
width: 100%;
height: 200px;
}
#app-loading-error {
font-size: 5em;
color: #fff;
}
#app-loading-title {
color: #fab;
font-size: 1.2em;
font-weight: lighter;
max-width: 500px;
margin: 0 auto;
}
#app-loading-title.error {
color: #fff;
}

459
app.js

@ -1,459 +0,0 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import Vue from "vue";
import "./app.css";
import Auth from "./auth.vue";
import { Color as ControlColor } from "./commands/color.js";
import { Commands } from "./commands/commands.js";
import { Controls } from "./commands/controls.js";
import { Presets } from "./commands/presets.js";
import * as ssh from "./commands/ssh.js";
import * as telnet from "./commands/telnet.js";
import "./common.css";
import * as sshctl from "./control/ssh.js";
import * as telnetctl from "./control/telnet.js";
import * as cipher from "./crypto.js";
import Home from "./home.vue";
import "./landing.css";
import Loading from "./loading.vue";
import { Socket } from "./socket.js";
import * as stream from "./stream/common";
import * as xhr from "./xhr.js";
const backendQueryRetryDelay = 2000;
const maxTimeDiff = 30000;
const updateIndicatorMaxDisplayTime = 3000;
const mainTemplate = `
<home
v-if="page == 'app'"
:host-path="hostPath"
:query="query"
:connection="socket"
:controls="controls"
:commands="commands"
:server-message="serverMessage"
:preset-data="presetData.presets"
:restricted-to-presets="presetData.restricted"
:view-port="viewPort"
@navigate-to="changeURLHash"
@tab-opened="tabOpened"
@tab-closed="tabClosed"
@tab-updated="tabUpdated"
></home>
<auth
v-else-if="page == 'auth'"
:error="authErr"
@auth="submitAuth"
></auth>
<loading v-else :error="loadErr"></loading>
`.trim();
const socksInterface = "/sshwifty/socket";
const socksVerificationInterface = socksInterface + "/verify";
const socksKeyTimeTruncater = 100 * 1000;
function startApp(rootEl) {
const pageTitle = document.title;
let uiControlColor = new ControlColor();
function getCurrentKeyMixer() {
return Number(
Math.trunc(new Date().getTime() / socksKeyTimeTruncater),
).toString();
}
async function buildSocketKey(privateKey) {
return new Uint8Array(
await cipher.hmac512(
stream.buildBufferFromString(privateKey),
stream.buildBufferFromString(getCurrentKeyMixer()),
),
).slice(0, 16);
}
new Vue({
el: rootEl,
components: {
loading: Loading,
auth: Auth,
home: Home,
},
data() {
return {
hostPath:
window.location.protocol +
"//" +
window.location.host +
window.location.pathname,
query:
window.location.hash.length > 0 &&
window.location.hash.indexOf("#") === 0
? window.location.hash.slice(1, window.location.hash.length)
: "",
page: "loading",
key: "",
serverMessage: "",
presetData: {
presets: new Presets([]),
restricted: false,
},
authErr: "",
loadErr: "",
socket: null,
controls: new Controls([
new telnetctl.Telnet(uiControlColor),
new sshctl.SSH(uiControlColor),
]),
commands: new Commands([new telnet.Command(), new ssh.Command()]),
tabUpdateIndicator: null,
viewPort: {
dim: {
width: 0,
height: 0,
renew(width, height) {
this.width = width;
this.height = height;
},
},
},
viewPortUpdaters: {
width: 0,
height: 0,
dimResizer: null,
},
};
},
watch: {
loadErr() {
this.isErrored()
? document.body.classList.add("app-error")
: document.body.classList.remove("app-error");
},
authErr() {
this.isErrored()
? document.body.classList.add("app-error")
: document.body.classList.remove("app-error");
},
},
mounted() {
const self = this;
self.tryInitialAuth();
self.viewPortUpdaters.dimResizer = () => {
self.viewPortUpdaters.height = window.innerHeight;
self.viewPortUpdaters.width = window.innerWidth;
self.$nextTick(() => {
self.viewPort.dim.renew(
self.viewPortUpdaters.width,
self.viewPortUpdaters.height,
);
});
};
window.addEventListener("resize", self.viewPortUpdaters.dimResizer);
},
beforeDestroy() {
window.removeEventListener("resize", self.viewPortUpdaters.dimResizer);
},
methods: {
changeTitleInfo(newTitleInfo) {
document.title = newTitleInfo + " " + pageTitle;
},
resetTitleInfo() {
document.title = pageTitle;
},
changeURLHash(newHash) {
window.location.hash = newHash;
},
isErrored() {
return this.authErr.length > 0 || this.loadErr.length > 0;
},
async getSocketAuthKey(privateKey) {
const enc = new TextEncoder(),
rTime = Number(Math.trunc(new Date().getTime() / 100000));
var finalKey = "";
if (privateKey.length <= 0) {
finalKey = "DEFAULT VERIFY KEY";
} else {
finalKey = privateKey;
}
return new Uint8Array(
await cipher.hmac512(enc.encode(finalKey), enc.encode(rTime)),
).slice(0, 32);
},
buildBackendSocketURLs() {
let r = {
webSocket: "",
keepAlive: "",
};
switch (location.protocol) {
case "https:":
r.webSocket = "wss://";
break;
default:
r.webSocket = "ws://";
}
r.webSocket += location.host + socksInterface;
r.keepAlive = location.protocol + "//" + location.host + socksInterface;
return r;
},
buildSocket(key, dialTimeout, heartbeatInterval) {
return new Socket(
this.buildBackendSocketURLs(),
key,
dialTimeout * 1000,
heartbeatInterval * 1000,
);
},
executeHomeApp(authResult, key) {
let authData = JSON.parse(authResult.data);
this.serverMessage = authData.server_message
? authData.server_message
: "";
this.presetData = {
presets: new Presets(authData.presets ? authData.presets : []),
restricted: authResult.onlyAllowPresetRemotes,
};
this.socket = this.buildSocket(
key,
authResult.timeout,
authResult.heartbeat,
);
this.page = "app";
},
async doAuth(privateKey) {
let result = await this.requestAuth(privateKey);
if (result.key) {
this.key = result.key;
}
return result;
},
async requestAuth(privateKey) {
let authKey =
!privateKey || !this.key
? null
: await this.getSocketAuthKey(privateKey);
let h = await xhr.get(socksVerificationInterface, {
"X-Key": authKey
? btoa(String.fromCharCode.apply(null, authKey))
: "",
});
let serverDate = h.getResponseHeader("Date");
return {
result: h.status,
key: h.getResponseHeader("X-Key"),
timeout: h.getResponseHeader("X-Timeout"),
heartbeat: h.getResponseHeader("X-Heartbeat"),
date: serverDate ? new Date(serverDate) : null,
data: h.responseText,
onlyAllowPresetRemotes:
h.getResponseHeader("X-OnlyAllowPresetRemotes") === "yes",
};
},
async tryInitialAuth() {
try {
let result = await this.doAuth("");
if (result.date) {
let serverTime = result.date.getTime(),
clientTime = new Date().getTime(),
timeDiff = Math.abs(serverTime - clientTime);
if (timeDiff > maxTimeDiff) {
this.loadErr =
"The time difference between this client " +
"and the backend server is beyond operational limit.\r\n\r\n" +
"Please try reload the page, and if the problem persisted, " +
"consider to adjust your local time so both the client and " +
"the server are running at same date time";
return;
}
}
let self = this;
switch (result.result) {
case 200:
this.executeHomeApp(result, {
async fetch() {
let result = await self.doAuth("");
if (result.result !== 200) {
throw new Error(
"Unable to fetch key from remote, unexpected " +
"error code: " +
result.result,
);
}
return await buildSocketKey(atob(result.key) + "+");
},
});
break;
case 403:
this.page = "auth";
break;
case 0:
setTimeout(() => {
this.tryInitialAuth();
}, backendQueryRetryDelay);
break;
default:
alert("Unexpected backend query status: " + result.result);
}
} catch (e) {
this.loadErr = "Unable to initialize client application: " + e;
}
},
async submitAuth(passphrase) {
this.authErr = "";
try {
let result = await this.doAuth(passphrase);
let self = this;
switch (result.result) {
case 200:
this.executeHomeApp(result, {
async fetch() {
let result = await self.doAuth(passphrase);
if (result.result !== 200) {
throw new Error(
"Unable to fetch key from remote, unexpected " +
"error code: " +
result.result,
);
}
return await buildSocketKey(
atob(result.key) + "+" + passphrase,
);
},
});
break;
case 403:
this.authErr = "Authentication has failed. Wrong passphrase?";
break;
default:
this.authErr =
"Unexpected backend query status: " + result.result;
}
} catch (e) {
this.authErr = "Unable to authenticate: " + e;
}
},
updateTabTitleInfo(tabs, updated) {
if (tabs.length <= 0) {
this.resetTitleInfo();
return;
}
this.changeTitleInfo("(" + tabs.length + (updated ? "*" : "") + ")");
},
tabOpened(tabs) {
this.tabUpdated(tabs);
},
tabClosed(tabs) {
if (tabs.length > 0) {
this.updateTabTitleInfo(tabs, this.tabUpdateIndicator !== null);
return;
}
if (this.tabUpdateIndicator) {
clearTimeout(this.tabUpdateIndicator);
this.tabUpdateIndicator = null;
}
this.updateTabTitleInfo(tabs, false);
},
tabUpdated(tabs) {
if (this.tabUpdateIndicator) {
clearTimeout(this.tabUpdateIndicator);
this.tabUpdateIndicator = null;
}
this.updateTabTitleInfo(tabs, true);
this.tabUpdateIndicator = setTimeout(() => {
this.tabUpdateIndicator = null;
this.updateTabTitleInfo(tabs, false);
}, updateIndicatorMaxDisplayTime);
},
},
});
}
function initializeClient() {
let landingRoot = document.getElementById("landing");
if (!landingRoot) {
return;
}
if (process.env.NODE_ENV === "development") {
console.log("Currently in Development environment");
}
window.addEventListener("unhandledrejection", function (e) {
console.error("Error:", e);
});
window.addEventListener("error", function (e) {
console.error("Error:", e);
});
landingRoot.parentNode.removeChild(landingRoot);
let normalRoot = document.createElement("div");
normalRoot.setAttribute("id", "app");
normalRoot.innerHTML = mainTemplate;
document.body.insertBefore(normalRoot, document.body.firstChild);
startApp(normalRoot);
}
window.addEventListener("load", initializeClient);
document.addEventListener("load", initializeClient);
document.addEventListener("DOMContentLoaded", initializeClient);

@ -1,126 +0,0 @@
<!--
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="auth">
<div id="auth-frame">
<div id="auth-content">
<h1>Authentication required</h1>
<form class="form1" action="javascript:;" method="POST" @submit="auth">
<fieldset>
<div
class="field"
:class="{
error: passphraseErr.length > 0 || error.length > 0,
}"
>
Passphrase
<input
v-model="passphrase"
v-focus="true"
:disabled="submitting"
type="password"
autocomplete="off"
name="field.field.name"
placeholder="----------"
autofocus="autofocus"
/>
<div
v-if="passphraseErr.length <= 0 && error.length <= 0"
class="message"
>
A valid password is required in order to use this
<a href="https://github.com/nirui/sshwifty">Sshwifty</a>
instance
</div>
<div v-else class="error">
{{ passphraseErr || error }}
</div>
</div>
<div class="field">
<button type="submit" :disabled="submitting" @click="auth">
Authenticate
</button>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
directives: {
focus: {
inserted(el, binding) {
if (!binding.value) {
return;
}
el.focus();
},
},
},
props: {
error: {
type: String,
default: "",
},
},
data() {
return {
submitting: false,
passphrase: "",
passphraseErr: "",
};
},
watch: {
error(newVal) {
if (newVal.length > 0) {
this.submitting = false;
}
},
},
mounted() {},
methods: {
auth() {
if (this.passphrase.length <= 0) {
this.passphraseErr = "Passphrase cannot be empty";
return;
}
if (this.submitting) {
return;
}
this.submitting = true;
this.passphraseErr = "";
this.$emit("auth", this.passphrase);
},
},
};
</script>

@ -1,118 +0,0 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/**
* Generate HMAC 512 of given data
*
* @param {Uint8Array} secret Secret key
* @param {Uint8Array} data Data to be HMAC'ed
*/
export async function hmac512(secret, data) {
const key = await window.crypto.subtle.importKey(
"raw",
secret,
{
name: "HMAC",
hash: { name: "SHA-512" },
},
false,
["sign", "verify"],
);
return window.crypto.subtle.sign(key.algorithm, key, data);
}
export const GCMNonceSize = 12;
export const GCMKeyBitLen = 128;
/**
* Build AES GCM Encryption/Decryption key
*
* @param {Uint8Array} keyData Key data
*/
export function buildGCMKey(keyData) {
return window.crypto.subtle.importKey(
"raw",
keyData,
{
name: "AES-GCM",
length: GCMKeyBitLen,
},
false,
["encrypt", "decrypt"],
);
}
/**
* Encrypt data
*
* @param {CryptoKey} key Key
* @param {Uint8Array} iv Nonce
* @param {Uint8Array} plaintext Data to be encrypted
*/
export function encryptGCM(key, iv, plaintext) {
return window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv, tagLength: GCMKeyBitLen },
key,
plaintext,
);
}
/**
* Decrypt data
*
* @param {CryptoKey} key Key
* @param {Uint8Array} iv Nonce
* @param {Uint8Array} cipherText Data to be decrypted
*/
export function decryptGCM(key, iv, cipherText) {
return window.crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv, tagLength: GCMKeyBitLen },
key,
cipherText,
);
}
/**
* generate Random nonce
*
*/
export function generateNonce() {
return window.crypto.getRandomValues(new Uint8Array(GCMNonceSize));
}
/**
* Increase nonce by one
*
* @param {Uint8Array} nonce Nonce data
*
* @returns {Uint8Array} New nonce
*
*/
export function increaseNonce(nonce) {
for (let i = nonce.length; i > 0; i--) {
nonce[i - 1]++;
if (nonce[i - 1] <= 0) {
continue;
}
break;
}
return nonce;
}

@ -1,37 +0,0 @@
<!--
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body class="app-error">
<div id="app-loading">
<div id="app-loading-frame">
<div id="app-loading-error">&times;</div>
<h1 id="app-loading-title" class="error">
Server was unable to complete the request
</h1>
</div>
</div>
</body>
</html>

@ -1,422 +0,0 @@
/*
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@charset "utf-8";
@import "~roboto-fontface/css/roboto/roboto-fontface.css";
@keyframes home-window-display-flash {
0% {
top: -2px;
opacity: 0;
box-shadow: 0 0 0 transparent;
}
20% {
height: 20px;
box-shadow: 0 0 50px #fff;
}
50% {
opacity: 0.5;
box-shadow: 0 0 10px #fff;
}
100% {
top: 100%;
height: 0;
opacity: 0;
box-shadow: 0 0 0 transparent;
}
}
.home-window-display {
}
.home-window-display::after {
opacity: 0;
z-index: 2;
content: " ";
display: block;
position: absolute;
width: 100%;
height: 0;
top: -2px;
left: 0;
right: 0;
background: #fff;
animation-name: home-window-display-flash;
animation-duration: 0.3s;
animation-iteration-count: 1;
box-shadow: 0 0 10px #fff;
}
#home {
height: 100%;
display: flex;
flex-direction: column;
font: 1em "Roboto", sans-serif;
}
#home-header {
flex: 0 0 40px;
font-size: 0.9em;
color: #fff;
width: 100%;
overflow: auto;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
#home-hd-title {
font-size: 1.1em;
padding: 0 0 0 20px;
font-weight: bold;
flex: 0 0 65px;
text-align: center;
overflow: hidden;
}
#home-hd-delay {
font-size: 0.95em;
display: flex;
flex: 0 0 70px;
flex-direction: row;
overflow: hidden;
margin: 0 10px;
color: #aaa;
text-decoration: none;
justify-items: center;
justify-content: center;
align-items: center;
}
@media (max-width: 768px) {
#home-hd-title {
padding: 0 0 0 10px;
}
#home-hd-delay {
flex: 0 0 60px;
}
}
#home-hd-delay-icon {
color: #bbb;
text-shadow: 0 0 3px #999;
transition: linear 0.2s color, text-shadow;
margin: 5px;
font-size: 0.54em;
}
#home-hd-delay-value {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
word-wrap: none;
}
@keyframes home-hd-delay-icon-flash {
0% {
opacity: 1;
}
10% {
opacity: 0;
}
20% {
opacity: 1;
}
30% {
opacity: 0;
}
90% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes home-hd-delay-icon-working {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#home-hd-delay-icon.green {
color: #1e8;
text-shadow: 0 0 3px #1e8;
}
#home-hd-delay-icon.yellow {
color: #ff4;
text-shadow: 0 0 3px #ff4;
}
#home-hd-delay-icon.orange {
color: #f80;
text-shadow: 0 0 3px #f80;
}
#home-hd-delay-icon.red {
color: #e11;
text-shadow: 0 0 3px #e11;
}
#home-hd-delay-icon.flash {
animation-name: home-hd-delay-icon-flash;
animation-duration: 1s;
animation-iteration-count: infinite;
}
#home-hd-delay-icon.working {
animation-name: home-hd-delay-icon-working;
animation-duration: 1.5s;
animation-iteration-count: infinite;
}
#home-hd-plus {
flex: 0 0;
padding: 0 13px;
text-decoration: none;
font-size: 22px;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
@keyframes home-hd-plus-icon-flash {
0% {
background: #a56;
}
20% {
background: #5a7;
}
40% {
background: #96a;
}
60% {
background: #379;
}
80% {
background: #da0;
}
100% {
background: #a56;
}
}
#home-hd-plus.working {
color: #fff;
background: #a56;
animation-name: home-hd-plus-icon-flash;
animation-duration: 10s;
animation-iteration-count: infinite;
animation-direction: normal;
transition: linear 2s background;
}
#home-hd-plus.working.intensify {
animation-duration: 3s;
}
#home-hd-tabs {
background: #333;
flex: auto;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
}
#home-hd-tabs-tabs {
flex: auto;
overflow: hidden;
}
#home-hd-tabs-tabs > li {
flex: 0 0 180px;
display: flex;
position: relative;
padding: 0 15px;
opacity: 0.5;
color: #999;
}
#home-hd-tabs-tabs > li::after {
content: " ";
display: block;
position: absolute;
bottom: 0;
right: 10px;
left: 10px;
height: 0;
transition: all 0.1s linear;
transition-property: height, right, left;
}
#home-hd-tabs-tabs > li.active::after {
right: 0;
left: 0;
}
#home-hd-tabs-tabs > li.updated::after {
background: #fff3;
height: 2px;
}
#home-hd-tabs-tabs > li.error::after {
background: #d55;
height: 2px;
}
#home-hd-tabs-tabs > li > span.title {
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
}
#home-hd-tabs-tabs > li > span.title > span.type {
display: inline-block;
font-size: 0.85em;
font-weight: bold;
margin-right: 3px;
text-transform: uppercase;
color: #fff;
background: #222;
padding: 1px 4px;
border-radius: 2px;
}
#home-hd-tabs-tabs > li > .icon-close {
display: none;
}
#home-hd-tabs-tabs > li.active {
color: #fff;
opacity: 1;
}
#home-hd-tabs-tabs > li.active > span.title {
padding-right: 15px;
}
#home-hd-tabs-tabs > li.active > .icon-close {
display: block;
position: absolute;
top: 50%;
right: 10px;
margin-top: -5px;
color: #fff6;
}
#home-hd-tabs-list {
display: flex;
font-size: 22px;
flex: 0 0;
padding: 0 13px;
flex-direction: column;
justify-content: center;
height: 100%;
text-decoration: none;
box-shadow: 0 0 3px #333;
}
#home-content {
flex: auto;
display: flex;
justify-content: center;
flex-direction: column;
color: #fff;
font-size: 1.2em;
}
@media (max-width: 768px) {
#home-content {
font-size: 1em;
}
}
#home-content-wrap {
max-width: 520px;
margin: 50px auto;
padding: 0 30px;
text-align: center;
}
#home-content h1 {
margin: 20px 0;
}
#home-content p {
margin: 10px 0;
font-size: 0.9em;
color: #eee;
line-height: 1.6;
}
#home-content p.secondary {
margin: 5px 0;
line-height: 1.5;
font-size: 0.7em;
color: #aaa;
}
#home-content p a {
color: #e9a;
}
#home-content hr {
height: 2px;
background: #3c3c3c;
border: none;
margin: 30px 0;
}
#home-content-connect {
padding: 5px;
display: inline-block;
width: 20px;
height: 20px;
cursor: pointer;
}

@ -1,617 +0,0 @@
<!--
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="home">
<header id="home-header">
<h1 id="home-hd-title">Sshwifty</h1>
<a id="home-hd-delay" href="javascript:;" @click="showDelayWindow">
<span
id="home-hd-delay-icon"
class="icon icon-point1"
:class="socket.classStyle"
></span>
<span v-if="socket.message.length > 0" id="home-hd-delay-value">{{
socket.message
}}</span>
</a>
<a
id="home-hd-plus"
class="icon icon-plus1"
href="javascript:;"
:class="{
working: connector.inputting,
intensify: connector.inputting && !windows.connect,
}"
@click="showConnectWindow"
></a>
<tabs
id="home-hd-tabs"
:tab="tab.current"
:tabs="tab.tabs"
tabs-class="tab1"
list-trigger-class="icon icon-more1"
@current="switchTab"
@retap="retapTab"
@list="showTabsWindow"
@close="closeTab"
></tabs>
</header>
<screens
id="home-content"
:screen="tab.current"
:screens="tab.tabs"
:view-port="viewPort"
@stopped="tabStopped"
@warning="tabWarning"
@info="tabInfo"
@updated="tabUpdated"
>
<div id="home-content-wrap">
<h1>Hi, this is Sshwifty</h1>
<p>
An Open Source Web SSH Client that enables you to connect to SSH
servers without downloading any additional software.
</p>
<p>
To get started, click the
<span
id="home-content-connect"
class="icon icon-plus1"
@click="showConnectWindow"
></span>
icon near the top left corner.
</p>
<div v-if="serverMessage.length > 0">
<hr />
<p class="secondary" v-html="serverMessage"></p>
</div>
</div>
</screens>
<connect-widget
:inputting="connector.inputting"
:display="windows.connect"
:connectors="connector.connectors"
:presets="presets"
:restricted-to-presets="restrictedToPresets"
:knowns="connector.knowns"
:knowns-launcher-builder="buildknownLauncher"
:knowns-export="exportKnowns"
:knowns-import="importKnowns"
:busy="connector.busy"
@display="windows.connect = $event"
@connector-select="connectNew"
@known-select="connectKnown"
@known-remove="removeKnown"
@preset-select="connectPreset"
@known-clear-session="clearSessionKnown"
>
<connector
:connector="connector.connector"
@cancel="cancelConnection"
@done="connectionSucceed"
>
</connector>
</connect-widget>
<status-widget
:class="socket.windowClass"
:display="windows.delay"
:status="socket.status"
@display="windows.delay = $event"
></status-widget>
<tab-window
:tab="tab.current"
:tabs="tab.tabs"
:display="windows.tabs"
tabs-class="tab1 tab1-list"
@display="windows.tabs = $event"
@current="switchTab"
@retap="retapTab"
@close="closeTab"
></tab-window>
</div>
</template>
<script>
import "./home.css";
import ConnectWidget from "./widgets/connect.vue";
import StatusWidget from "./widgets/status.vue";
import Connector from "./widgets/connector.vue";
import Tabs from "./widgets/tabs.vue";
import TabWindow from "./widgets/tab_window.vue";
import Screens from "./widgets/screens.vue";
import * as home_socket from "./home_socketctl.js";
import * as home_history from "./home_historyctl.js";
import * as presets from "./commands/presets.js";
const BACKEND_CONNECT_ERROR =
"Unable to connect to the Sshwifty backend server: ";
const BACKEND_REQUEST_ERROR = "Unable to perform request: ";
export default {
components: {
"connect-widget": ConnectWidget,
"status-widget": StatusWidget,
connector: Connector,
tabs: Tabs,
"tab-window": TabWindow,
screens: Screens,
},
props: {
hostPath: {
type: String,
default: "",
},
query: {
type: String,
default: "",
},
connection: {
type: Object,
default: () => null,
},
controls: {
type: Object,
default: () => null,
},
commands: {
type: Object,
default: () => null,
},
serverMessage: {
type: String,
default: "",
},
presetData: {
type: Object,
default: () => new presets.Presets([]),
},
restrictedToPresets: {
type: Boolean,
default: () => false,
},
viewPort: {
type: Object,
default: () => null,
},
},
data() {
let history = home_history.build(this);
return {
ticker: null,
windows: {
delay: false,
connect: false,
tabs: false,
},
socket: home_socket.build(this),
connector: {
historyRec: history,
connector: null,
connectors: this.commands.all(),
inputting: false,
acquired: false,
busy: false,
knowns: history.all(),
},
presets: this.commands.mergePresets(this.presetData),
tab: {
current: -1,
lastID: 0,
tabs: [],
},
};
},
mounted() {
this.ticker = setInterval(() => {
this.tick();
}, 1000);
if (this.query.length > 1 && this.query.indexOf("+") === 0) {
this.connectLaunch(this.query.slice(1, this.query.length), (success) => {
if (!success) {
return;
}
this.$emit("navigate-to", "");
});
}
window.addEventListener("beforeunload", this.onBrowserClose);
},
beforeDestroy() {
window.removeEventListener("beforeunload", this.onBrowserClose);
if (this.ticker === null) {
clearInterval(this.ticker);
this.ticker = null;
}
},
methods: {
onBrowserClose(e) {
if (this.tab.current < 0) {
return undefined;
}
const msg = "Some tabs are still open, are you sure you want to exit?";
(e || window.event).returnValue = msg;
return msg;
},
tick() {
let now = new Date();
this.socket.update(now, this);
},
closeAllWindow(e) {
for (let i in this.windows) {
this.windows[i] = false;
}
},
showDelayWindow() {
this.closeAllWindow();
this.windows.delay = true;
},
showConnectWindow() {
this.closeAllWindow();
this.windows.connect = true;
},
showTabsWindow() {
this.closeAllWindow();
this.windows.tabs = true;
},
async getStreamThenRun(run, end) {
let errStr = null;
try {
let conn = await this.connection.get(this.socket);
try {
run(conn);
} catch (e) {
errStr = BACKEND_REQUEST_ERROR + e;
process.env.NODE_ENV === "development" && console.trace(e);
}
} catch (e) {
errStr = BACKEND_CONNECT_ERROR + e;
process.env.NODE_ENV === "development" && console.trace(e);
}
end();
if (errStr !== null) {
alert(errStr);
}
},
runConnect(callback) {
if (this.connector.acquired) {
return;
}
this.connector.acquired = true;
this.connector.busy = true;
this.getStreamThenRun(
(stream) => {
this.connector.busy = false;
callback(stream);
},
() => {
this.connector.busy = false;
this.connector.acquired = false;
},
);
},
connectNew(connector) {
const self = this;
self.runConnect((stream) => {
self.connector.connector = {
id: connector.id(),
name: connector.name(),
description: connector.description(),
wizard: connector.wizard(
stream,
self.controls,
self.connector.historyRec,
presets.emptyPreset(),
null,
false,
() => {},
),
};
self.connector.inputting = true;
});
},
connectPreset(preset) {
const self = this;
self.runConnect((stream) => {
self.connector.connector = {
id: preset.command.id(),
name: preset.command.name(),
description: preset.command.description(),
wizard: preset.command.wizard(
stream,
self.controls,
self.connector.historyRec,
preset.preset,
null,
[],
() => {},
),
};
self.connector.inputting = true;
});
},
getConnectorByType(type) {
let connector = null;
for (let c in this.connector.connectors) {
if (this.connector.connectors[c].name() !== type) {
continue;
}
connector = this.connector.connectors[c];
}
return connector;
},
connectKnown(known) {
const self = this;
self.runConnect((stream) => {
let connector = self.getConnectorByType(known.type);
if (!connector) {
alert("Unknown connector: " + known.type);
self.connector.inputting = false;
return;
}
self.connector.connector = {
id: connector.id(),
name: connector.name(),
description: connector.description(),
wizard: connector.execute(
stream,
self.controls,
self.connector.historyRec,
known.data,
known.session,
known.keptSessions,
() => {
self.connector.knowns = self.connector.historyRec.all();
},
),
};
self.connector.inputting = true;
});
},
parseConnectLauncher(ll) {
let llSeparatorIdx = ll.indexOf(":");
// Type must contain at least one charater
if (llSeparatorIdx <= 0) {
throw new Error("Invalid Launcher string");
}
return {
type: ll.slice(0, llSeparatorIdx),
query: ll.slice(llSeparatorIdx + 1, ll.length),
};
},
connectLaunch(launcher, done) {
this.showConnectWindow();
this.runConnect((stream) => {
let ll = this.parseConnectLauncher(launcher),
connector = this.getConnectorByType(ll.type);
if (!connector) {
alert("Unknown connector: " + ll.type);
this.connector.inputting = false;
return;
}
const self = this;
this.connector.connector = {
id: connector.id(),
name: connector.name(),
description: connector.description(),
wizard: connector.launch(
stream,
this.controls,
this.connector.historyRec,
ll.query,
(n) => {
self.connector.knowns = self.connector.historyRec.all();
done(n.data().success);
},
),
};
this.connector.inputting = true;
});
},
buildknownLauncher(known) {
let connector = this.getConnectorByType(known.type);
if (!connector) {
return;
}
return this.hostPath + "#+" + connector.launcher(known.data);
},
exportKnowns() {
return this.connector.historyRec.export();
},
importKnowns(d) {
this.connector.historyRec.import(d);
this.connector.knowns = this.connector.historyRec.all();
},
removeKnown(uid) {
this.connector.historyRec.del(uid);
this.connector.knowns = this.connector.historyRec.all();
},
clearSessionKnown(uid) {
this.connector.historyRec.clearSession(uid);
this.connector.knowns = this.connector.historyRec.all();
},
cancelConnection() {
this.connector.inputting = false;
this.connector.acquired = false;
},
connectionSucceed(data) {
this.connector.inputting = false;
this.connector.acquired = false;
this.windows.connect = false;
this.addToTab(data);
this.$emit("tab-opened", this.tab.tabs);
},
async addToTab(data) {
await this.switchTab(
this.tab.tabs.push({
id: this.tab.lastID++,
name: data.name,
info: data.info,
control: data.control,
ui: data.ui,
toolbar: false,
indicator: {
level: "",
message: "",
updated: false,
},
status: {
closing: false,
},
}) - 1,
);
},
removeFromTab(index) {
let isLast = index === this.tab.tabs.length - 1;
this.tab.tabs.splice(index, 1);
this.tab.current = isLast ? this.tab.tabs.length - 1 : index;
},
async switchTab(to) {
if (this.tab.current >= 0) {
await this.tab.tabs[this.tab.current].control.disabled();
}
this.tab.current = to;
this.tab.tabs[this.tab.current].indicator.updated = false;
await this.tab.tabs[this.tab.current].control.enabled();
},
async retapTab(tab) {
this.tab.tabs[tab].toolbar = !this.tab.tabs[tab].toolbar;
await this.tab.tabs[tab].control.retap(this.tab.tabs[tab].toolbar);
},
async closeTab(index) {
if (this.tab.tabs[index].status.closing) {
return;
}
this.tab.tabs[index].status.closing = true;
try {
this.tab.tabs[index].control.disabled();
await this.tab.tabs[index].control.close();
} catch (e) {
alert("Cannot close tab due to error: " + e);
process.env.NODE_ENV === "development" && console.trace(e);
}
this.removeFromTab(index);
this.$emit("tab-closed", this.tab.tabs);
},
tabStopped(index, reason) {
if (reason !== null) {
this.tab.tabs[index].indicator.message = "" + reason;
this.tab.tabs[index].indicator.level = "error";
} else {
this.tab.tabs[index].indicator.message = "";
this.tab.tabs[index].indicator.level = "";
}
},
tabMessage(index, msg, type) {
if (msg.toDismiss) {
if (
this.tab.tabs[index].indicator.message !== msg.text ||
this.tab.tabs[index].indicator.level !== type
) {
return;
}
this.tab.tabs[index].indicator.message = "";
this.tab.tabs[index].indicator.level = "";
return;
}
this.tab.tabs[index].indicator.message = msg.text;
this.tab.tabs[index].indicator.level = type;
},
tabWarning(index, msg) {
this.tabMessage(index, msg, "warning");
},
tabInfo(index, msg) {
this.tabMessage(index, msg, "info");
},
tabUpdated(index) {
this.$emit("tab-updated", this.tab.tabs);
this.tab.tabs[index].indicator.updated = index !== this.tab.current;
},
},
};
</script>

@ -1,64 +0,0 @@
<!--
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Sshwifty Web SSH Client</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="landing">
<div id="landing-message">
<div id="landing-message-logo"></div>
<h1 id="landing-message-title">Loading Sshwifty</h1>
<div id="landing-message-info">
<p>
Client is currently being loaded. Should only take a few seconds,
please wait
</p>
<noscript>
<p>
Also, surely you smart people knows that application such like
this one require JavaScript to run :)
</p>
</noscript>
<p class="copy copy-first">
Copyright &copy; 2019-2023 Ni Rui &lt;ranqus@gmail.com&gt;
</p>
<p class="copy">
<a href="https://github.com/nirui/sshwifty" target="blank">
Source code
</a>
<a href="/sshwifty/assets/DEPENDENCIES.md" target="blank">
Third-party
</a>
<a href="/sshwifty/assets/README.md" target="blank"> Readme </a>
<a href="/sshwifty/assets/LICENSE.md" target="blank"> License </a>
</p>
</div>
</div>
</div>
</body>
</html>

@ -1,132 +0,0 @@
/*
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@charset "utf-8";
body.landing {
background: #945;
}
#landing {
min-height: 100%;
font-family: Arial, Helvetica, sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
justify-items: center;
color: #fff;
text-align: center;
}
#landing-message {
font-size: 0.9em;
max-width: 500px;
margin: 0 auto;
padding: 50px;
flex: 0 0;
}
#landing-message a {
text-decoration: none;
color: #fab;
}
#landing-message p {
margin: 10px 0;
}
#landing-message p.copy {
margin: 20px 0 10px 0;
color: #fab;
}
#landing-message p.copy.copy-first {
margin-top: 50px;
}
#landing-message p.copy a {
border: 1px solid #fab;
display: inline-block;
padding: 3px 7px;
margin: 5px;
border-radius: 5px;
line-height: initial;
}
#landing-message-logo {
background: url("./widgets/busy.svg") center center no-repeat;
background-size: contain;
width: 100%;
height: 200px;
}
#landing-message-info {
margin-top: 50px;
font-size: 0.9em;
}
#auth {
width: 100%;
min-height: 100%;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
#auth-frame {
flex: 0 0;
padding: 10px;
}
#auth-content {
background: #333;
box-shadow: 0 0 3px #111;
padding: 30px;
max-width: 380px;
margin: 0 auto;
font-size: 1em;
}
#auth-content > h1 {
margin-bottom: 20px;
color: #fab;
font-size: 1.2em;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 2px;
}
#auth-content > h1:after {
content: "\2731";
margin-left: 5px;
}
#auth-content > form {
font-size: 0.9em;
}
#auth-content > form .field:last-child {
margin-top: 30px;
}
#auth-content > form > fieldset {
margin-top: 30px;
}

@ -1,45 +0,0 @@
<!--
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="app-loading">
<div id="app-loading-frame">
<div v-if="error.length <= 0" id="app-loading-icon"></div>
<div v-else id="app-loading-error">&times;</div>
<h1 v-if="error.length <= 0" id="app-loading-title">
Preparing client application
</h1>
<h1 v-else id="app-loading-title" class="error">
{{ error }}
</h1>
</div>
</div>
</template>
<script>
export default {
props: {
error: {
type: String,
default: "",
},
},
};
</script>

@ -1,4 +0,0 @@
user-agent: *
Disallow: /
Allow: /$

@ -1,397 +0,0 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import * as crypt from "./crypto.js";
import * as reader from "./stream/reader.js";
import * as sender from "./stream/sender.js";
import * as streams from "./stream/streams.js";
import * as xhr from "./xhr.js";
export const ECHO_FAILED = streams.ECHO_FAILED;
const maxSenderDelay = 200;
const minSenderDelay = 30;
class Dial {
/**
* constructor
*
* @param {string} address Address to the Websocket server
* @param {number} Dial timeout
* @param {object} privateKey String key that will be used to encrypt and
* decrypt socket traffic
*
*/
constructor(address, timeout, privateKey) {
this.address = address;
this.timeout = timeout;
this.privateKey = privateKey;
this.keepAliveTicker = null;
}
/**
* Connect to the remote server
*
* @param {string} address Target URL address
* @param {number} timeout Connect timeout
*
* @returns {Promise<WebSocket>} When connection is established
*
*/
connect(address, timeout) {
const self = this;
return new Promise((resolve, reject) => {
let ws = new WebSocket(address.webSocket),
promised = false,
timeoutTimer = setTimeout(() => {
ws.close();
}, timeout),
myRes = (w) => {
if (promised) {
return;
}
clearTimeout(timeoutTimer);
promised = true;
return resolve(w);
},
myRej = (e) => {
if (promised) {
return;
}
clearTimeout(timeoutTimer);
promised = true;
return reject(e);
};
if (!self.keepAliveTicker) {
self.keepAliveTicker = setInterval(() => {
xhr.options(address.keepAlive, {});
}, self.timeout);
}
ws.addEventListener("open", (_event) => {
myRes(ws);
});
ws.addEventListener("close", (event) => {
event.toString = () => {
return "WebSocket Error (" + event.code + ")";
};
myRej(event);
clearInterval(self.keepAliveTicker);
self.keepAliveTicker = null;
});
ws.addEventListener("error", (_event) => {
ws.close();
clearInterval(self.keepAliveTicker);
self.keepAliveTicker = null;
});
});
}
/**
* Build an socket encrypt and decrypt key string
*
*/
async buildKeyString() {
return this.privateKey.fetch();
}
/**
* Build encrypt and decrypt key
*
*/
async buildKey() {
let kStr = await this.buildKeyString();
return await crypt.buildGCMKey(kStr);
}
/**
* Connect to the server
*
* @param {object} callbacks Callbacks
*
* @returns {object} A pair of ReadWriter which can be used to read and
* send data to the underlaying websocket connection
*
*/
async dial(callbacks) {
let ws = await this.connect(this.address, this.timeout);
try {
let rd = new reader.Reader(new reader.Multiple(() => {}), (data) => {
return new Promise((resolve) => {
let bufferReader = new FileReader();
bufferReader.onload = (event) => {
let d = new Uint8Array(event.target.result);
resolve(d);
callbacks.inboundUnpacked(d);
};
bufferReader.readAsArrayBuffer(data);
});
});
ws.addEventListener("message", (event) => {
callbacks.inbound(event.data);
rd.feed(event.data);
});
ws.addEventListener("error", (event) => {
event.toString = () => {
return (
"WebSocket Error (" + (event.code ? event.code : "Unknown") + ")"
);
};
rd.closeWithReason(event);
});
ws.addEventListener("close", (_event) => {
rd.closeWithReason("Connection is closed");
});
let sdDataConvert = (rawData) => {
return rawData;
},
getSdDataConvert = () => {
return sdDataConvert;
},
sd = new sender.Sender(
async (rawData) => {
try {
let data = await getSdDataConvert()(rawData);
ws.send(data.buffer);
callbacks.outbound(data);
} catch (e) {
ws.close();
rd.closeWithReason(e);
if (process.env.NODE_ENV === "development") {
console.error(e);
}
throw e;
}
},
4096 - 64, // Server has a 4096 bytes receive buffer, can be no greater,
minSenderDelay, // 30ms input delay
10, // max 10 buffered requests
);
let senderNonce = crypt.generateNonce();
sd.send(senderNonce);
let receiverNonce = await reader.readN(rd, crypt.GCMNonceSize);
let key = await this.buildKey();
sdDataConvert = async (rawData) => {
let encoded = await crypt.encryptGCM(key, senderNonce, rawData);
crypt.increaseNonce(senderNonce);
let dataToSend = new Uint8Array(encoded.byteLength + 2);
dataToSend[0] = (encoded.byteLength >> 8) & 0xff;
dataToSend[1] = encoded.byteLength & 0xff;
dataToSend.set(new Uint8Array(encoded), 2);
return dataToSend;
};
let cgmReader = new reader.Multiple(async (r) => {
try {
let dSizeBytes = await reader.readN(rd, 2),
dSize = 0;
dSize = dSizeBytes[0];
dSize <<= 8;
dSize |= dSizeBytes[1];
let decoded = await crypt.decryptGCM(
key,
receiverNonce,
await reader.readN(rd, dSize),
);
crypt.increaseNonce(receiverNonce);
r.feed(
new reader.Buffer(new Uint8Array(decoded), () => {}),
() => {},
);
} catch (e) {
r.closeWithReason(e);
}
});
return {
reader: cgmReader,
sender: sd,
ws: ws,
};
} catch (e) {
ws.close();
throw e;
}
}
}
export class Socket {
/**
* constructor
*
* @param {string} address Address of the WebSocket server
* @param {object} privateKey String key that will be used to encrypt and
* decrypt socket traffic
* @param {number} timeout Dial timeout
* @param {number} echoInterval Echo interval
*/
constructor(address, privateKey, timeout, echoInterval) {
this.dial = new Dial(address, timeout, privateKey);
this.echoInterval = echoInterval;
this.streamHandler = null;
}
/**
* Return a stream handler
*
* @param {object} callbacks A group of callbacks to call when needed
*
* @returns {Promise<streams.Streams>} The stream manager
*
*/
async get(callbacks) {
let self = this;
if (this.streamHandler) {
return this.streamHandler;
}
callbacks.connecting();
const receiveToPauseFactor = 6,
minReceivedToPause = 1024 * 16;
let streamPaused = false,
currentReceived = 0,
currentUnpacked = 0;
const shouldPause = () => {
return (
currentReceived > minReceivedToPause &&
currentReceived > currentUnpacked * receiveToPauseFactor
);
};
try {
let conn = await this.dial.dial({
inbound(data) {
currentReceived += data.size;
callbacks.traffic(data.size, 0);
},
inboundUnpacked(data) {
currentUnpacked += data.length;
if (currentUnpacked >= currentReceived) {
currentUnpacked = 0;
currentReceived = 0;
}
if (self.streamHandler !== null) {
if (streamPaused && !shouldPause()) {
streamPaused = false;
self.streamHandler.resume();
return;
} else if (!streamPaused && shouldPause()) {
streamPaused = true;
self.streamHandler.pause();
return;
}
}
},
outbound(data) {
callbacks.traffic(0, data.length);
},
});
let streamHandler = new streams.Streams(conn.reader, conn.sender, {
echoInterval: self.echoInterval,
echoUpdater(delay) {
const sendDelay = delay / 2;
if (sendDelay > maxSenderDelay) {
conn.sender.setDelay(maxSenderDelay);
} else if (sendDelay < minSenderDelay) {
conn.sender.setDelay(minSenderDelay);
} else {
conn.sender.setDelay(sendDelay);
}
return callbacks.echo(delay);
},
cleared(e) {
if (self.streamHandler === null) {
return;
}
self.streamHandler = null;
// Close connection first otherwise we may
// risk sending things out
conn.ws.close();
callbacks.close(e);
},
});
callbacks.connected();
streamHandler.serve().catch((e) => {
if (process.env.NODE_ENV !== "development") {
return;
}
console.trace(e);
});
this.streamHandler = streamHandler;
} catch (e) {
callbacks.failed(e);
throw e;
}
return this.streamHandler;
}
}
Loading…
Cancel
Save