clean up
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;
|
||||
}
|
@ -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">×</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 © 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
</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">×</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…
Reference in New Issue