605 lines
15 KiB
Vue
605 lines
15 KiB
Vue
<!--
|
|
// Sshwifty - A Web SSH client
|
|
//
|
|
// Copyright (C) 2019-2022 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">Shemesh Terminal</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>
|
|
|
|
</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>
|
|
|
|
<hr />
|
|
|
|
<p class="secondary">
|
|
Programmers in China launched an online campaign against
|
|
<a
|
|
href="https://en.wikipedia.org/wiki/996_working_hour_system"
|
|
target="blank"
|
|
>implicitly forced overtime work</a
|
|
>. Sshwifty wouldn't exist if its author must work such extreme
|
|
hours. If you're benefiting from hobbyist projects like this one,
|
|
please consider
|
|
<a
|
|
href="https://github.com/996icu/996.ICU/#what-can-i-do"
|
|
target="blank"
|
|
>supporting the action</a
|
|
>.
|
|
</p>
|
|
</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,
|
|
},
|
|
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>
|