Implemented the host name auto suggestion, and added Preset feature
This commit is contained in:
116
ui/app.js
116
ui/app.js
@@ -15,28 +15,24 @@
|
||||
// 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 "./common.css";
|
||||
import "./app.css";
|
||||
import "./landing.css";
|
||||
|
||||
import { Socket } from "./socket.js";
|
||||
|
||||
import Vue from "vue";
|
||||
import Home from "./home.vue";
|
||||
import "./app.css";
|
||||
import Auth from "./auth.vue";
|
||||
import Loading from "./loading.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 { Controls } from "./commands/controls.js";
|
||||
import { Color as ControlColor } from "./commands/color.js";
|
||||
import * as telnetctl from "./control/telnet.js";
|
||||
import "./common.css";
|
||||
import * as sshctl from "./control/ssh.js";
|
||||
|
||||
import * as xhr from "./xhr.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 xhr from "./xhr.js";
|
||||
|
||||
const backendQueryRetryDelay = 2000;
|
||||
|
||||
@@ -52,6 +48,8 @@ const mainTemplate = `
|
||||
:connection="socket"
|
||||
:controls="controls"
|
||||
:commands="commands"
|
||||
:preset-data="presetData.presets"
|
||||
:restricted-to-presets="presetData.restricted"
|
||||
@navigate-to="changeURLHash"
|
||||
@tab-opened="tabOpened"
|
||||
@tab-closed="tabClosed"
|
||||
@@ -66,6 +64,7 @@ const mainTemplate = `
|
||||
`.trim();
|
||||
|
||||
const socksInterface = "/sshwifty/socket";
|
||||
const socksVerificationInterface = socksInterface + "/verify";
|
||||
|
||||
function startApp(rootEl) {
|
||||
const pageTitle = document.title;
|
||||
@@ -93,6 +92,10 @@ function startApp(rootEl) {
|
||||
: "",
|
||||
page: "loading",
|
||||
key: "",
|
||||
presetData: {
|
||||
presets: new Presets([]),
|
||||
restricted: false
|
||||
},
|
||||
authErr: "",
|
||||
loadErr: "",
|
||||
socket: null,
|
||||
@@ -163,6 +166,18 @@ function startApp(rootEl) {
|
||||
heartbeatInterval * 1000
|
||||
);
|
||||
},
|
||||
executeHomeApp(authResult, key) {
|
||||
this.presetData = {
|
||||
presets: new Presets(JSON.parse(authResult.data)),
|
||||
restricted: authResult.onlyAllowPresetRemotes
|
||||
};
|
||||
this.socket = this.buildSocket(
|
||||
key,
|
||||
authResult.timeout,
|
||||
authResult.heartbeat
|
||||
);
|
||||
this.page = "app";
|
||||
},
|
||||
async tryInitialAuth() {
|
||||
try {
|
||||
let result = await this.doAuth("");
|
||||
@@ -188,35 +203,30 @@ function startApp(rootEl) {
|
||||
|
||||
switch (result.result) {
|
||||
case 200:
|
||||
this.socket = this.buildSocket(
|
||||
{
|
||||
data: result.key,
|
||||
async fetch() {
|
||||
if (this.data) {
|
||||
let dKey = this.data;
|
||||
this.executeHomeApp(result, {
|
||||
data: result.key,
|
||||
async fetch() {
|
||||
if (this.data) {
|
||||
let dKey = this.data;
|
||||
|
||||
this.data = null;
|
||||
this.data = null;
|
||||
|
||||
return dKey;
|
||||
}
|
||||
|
||||
let result = await self.doAuth("");
|
||||
|
||||
if (result.result !== 200) {
|
||||
throw new Error(
|
||||
"Unable to fetch key from remote, unexpected " +
|
||||
"error code: " +
|
||||
result.result
|
||||
);
|
||||
}
|
||||
|
||||
return result.key;
|
||||
return dKey;
|
||||
}
|
||||
},
|
||||
result.timeout,
|
||||
result.heartbeat
|
||||
);
|
||||
this.page = "app";
|
||||
|
||||
let result = await self.doAuth("");
|
||||
|
||||
if (result.result !== 200) {
|
||||
throw new Error(
|
||||
"Unable to fetch key from remote, unexpected " +
|
||||
"error code: " +
|
||||
result.result
|
||||
);
|
||||
}
|
||||
|
||||
return result.key;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 403:
|
||||
@@ -251,7 +261,7 @@ function startApp(rootEl) {
|
||||
? null
|
||||
: await this.getSocketAuthKey(privateKey, this.key);
|
||||
|
||||
let h = await xhr.head(socksInterface, {
|
||||
let h = await xhr.get(socksVerificationInterface, {
|
||||
"X-Key": authKey ? btoa(String.fromCharCode.apply(null, authKey)) : ""
|
||||
});
|
||||
|
||||
@@ -262,7 +272,10 @@ function startApp(rootEl) {
|
||||
key: h.getResponseHeader("X-Key"),
|
||||
timeout: h.getResponseHeader("X-Timeout"),
|
||||
heartbeat: h.getResponseHeader("X-Heartbeat"),
|
||||
date: serverDate ? new Date(serverDate) : null
|
||||
date: serverDate ? new Date(serverDate) : null,
|
||||
data: h.responseText,
|
||||
onlyAllowPresetRemotes:
|
||||
h.getResponseHeader("X-OnlyAllowPresetRemotes") === "yes"
|
||||
};
|
||||
},
|
||||
async submitAuth(passphrase) {
|
||||
@@ -273,17 +286,12 @@ function startApp(rootEl) {
|
||||
|
||||
switch (result.result) {
|
||||
case 200:
|
||||
this.socket = this.buildSocket(
|
||||
{
|
||||
data: passphrase,
|
||||
fetch() {
|
||||
return this.data;
|
||||
}
|
||||
},
|
||||
result.timeout,
|
||||
result.heartbeat
|
||||
);
|
||||
this.page = "app";
|
||||
this.executeHomeApp(result, {
|
||||
data: passphrase,
|
||||
fetch() {
|
||||
return this.data;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 403:
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
// 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 Exception from "./exception.js";
|
||||
import * as subscribe from "../stream/subscribe.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as presets from "./presets.js";
|
||||
|
||||
export const NEXT_PROMPT = 1;
|
||||
export const NEXT_WAIT = 2;
|
||||
@@ -131,8 +132,12 @@ const defField = {
|
||||
type: "",
|
||||
value: "",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(v) {
|
||||
return "OK";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,19 +160,21 @@ export function field(def, f) {
|
||||
}
|
||||
|
||||
for (let i in f) {
|
||||
if (typeof n[i] !== typeof f[i]) {
|
||||
throw new Exception(
|
||||
'Field data type for "' +
|
||||
i +
|
||||
'" was not unmatched. Expecting "' +
|
||||
typeof def[i] +
|
||||
'", got "' +
|
||||
typeof f[i] +
|
||||
'" instead'
|
||||
);
|
||||
if (typeof n[i] === typeof f[i]) {
|
||||
n[i] = f[i];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
n[i] = f[i];
|
||||
throw new Exception(
|
||||
'Field data type for "' +
|
||||
i +
|
||||
'" was unmatched. Expecting "' +
|
||||
typeof n[i] +
|
||||
'", got "' +
|
||||
typeof f[i] +
|
||||
'" instead'
|
||||
);
|
||||
}
|
||||
|
||||
if (!n["name"]) {
|
||||
@@ -206,6 +213,31 @@ export function fields(definitions, fs) {
|
||||
return fss;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command fields with preset data
|
||||
*
|
||||
* @param {object} definitions Definition of a group of fields
|
||||
* @param {object} fieldsData field data object, formated like a `defField`
|
||||
* @param {presets.Preset} presetData Preset data
|
||||
*
|
||||
* @returns {object}
|
||||
*
|
||||
*/
|
||||
export function fieldsWithPreset(definitions, fieldsData, presetData) {
|
||||
let newFields = fields(definitions, fieldsData);
|
||||
|
||||
for (let i in newFields) {
|
||||
try {
|
||||
newFields[i].value = presetData.meta(newFields[i].name);
|
||||
newFields[i].readonly = true;
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return newFields;
|
||||
}
|
||||
|
||||
class Prompt {
|
||||
/**
|
||||
* constructor
|
||||
@@ -457,7 +489,7 @@ class Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {function} builder Command builder
|
||||
* @param {object} built Command executer
|
||||
* @param {subscribe.Subscribe} subs Wizard step subscriber
|
||||
* @param {function} done Callback which will be called when the wizard
|
||||
* is done
|
||||
@@ -468,6 +500,8 @@ class Wizard {
|
||||
this.subs = subs;
|
||||
this.done = done;
|
||||
this.closed = false;
|
||||
|
||||
this.built.run();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -583,8 +617,14 @@ class Builder {
|
||||
*/
|
||||
constructor(command) {
|
||||
this.cid = command.id();
|
||||
this.builder = (n, i, r, u, y, x, l) => {
|
||||
return command.builder(n, i, r, u, y, x, l);
|
||||
this.represeter = n => {
|
||||
return command.represet(n);
|
||||
};
|
||||
this.wizarder = (n, i, r, u, y, x, l) => {
|
||||
return command.wizard(n, i, r, u, y, x, l);
|
||||
};
|
||||
this.executer = (n, i, r, u, y, x, l) => {
|
||||
return command.execute(n, i, r, u, y, x, l);
|
||||
};
|
||||
this.launchCmd = (n, i, r, u, y, x) => {
|
||||
return command.launch(n, i, r, u, y, x);
|
||||
@@ -638,7 +678,38 @@ class Builder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command wizard
|
||||
* Execute an automatic command wizard
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {function} done Callback which will be called when wizard is done
|
||||
*
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
wizard(streams, controls, history, preset, session, done) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.wizarder(
|
||||
new Info(this),
|
||||
preset,
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
),
|
||||
subs,
|
||||
done
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an automatic command wizard
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
@@ -650,11 +721,11 @@ class Builder {
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
build(streams, controls, history, config, session, done) {
|
||||
execute(streams, controls, history, config, session, done) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.builder(
|
||||
this.executer(
|
||||
new Info(this),
|
||||
config,
|
||||
session,
|
||||
@@ -707,19 +778,44 @@ class Builder {
|
||||
launcher(config) {
|
||||
return this.name() + ":" + encodeURI(this.launcherCmd(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconfigure the preset data for the command wizard
|
||||
*
|
||||
* @param {presets.Preset} n preset
|
||||
*
|
||||
* @return {presets.Preset} modified new preset
|
||||
*/
|
||||
represet(n) {
|
||||
return this.represeter(n);
|
||||
}
|
||||
}
|
||||
|
||||
export class Preset {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {presets.Preset} preset preset
|
||||
* @param {Builder} command executor
|
||||
*
|
||||
*/
|
||||
constructor(preset, command) {
|
||||
this.preset = preset;
|
||||
this.command = command;
|
||||
}
|
||||
}
|
||||
|
||||
export class Commands {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {[]object} commands Command array
|
||||
* @param {Array<object>} commands Command array
|
||||
*
|
||||
*/
|
||||
constructor(commands) {
|
||||
this.commands = [];
|
||||
|
||||
for (let i in commands) {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
this.commands.push(new Builder(commands[i]));
|
||||
}
|
||||
}
|
||||
@@ -727,7 +823,7 @@ export class Commands {
|
||||
/**
|
||||
* Return all commands
|
||||
*
|
||||
* @returns {[]Builder} A group of command
|
||||
* @returns {Array<Builder>} A group of command
|
||||
*
|
||||
*/
|
||||
all() {
|
||||
@@ -745,4 +841,28 @@ export class Commands {
|
||||
select(id) {
|
||||
return this.commands[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns presets with merged command
|
||||
*
|
||||
* @param {presets.Presets} ps
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
mergePresets(ps) {
|
||||
let pp = [];
|
||||
|
||||
for (let i = 0; i < this.commands.length; i++) {
|
||||
const fetched = ps.fetch(this.commands[i].name());
|
||||
|
||||
for (let j = 0; j < fetched.length; j++) {
|
||||
pp.push(
|
||||
new Preset(this.commands[i].represet(fetched[j]), this.commands[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return pp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
|
||||
import * as command from "./commands.js";
|
||||
|
||||
function metaContains(data, metaName, valContains) {
|
||||
switch (typeof data[metaName]) {
|
||||
case "string":
|
||||
return data[metaName].indexOf(valContains) >= 0;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class History {
|
||||
/**
|
||||
* constructor
|
||||
@@ -206,4 +216,39 @@ export class History {
|
||||
|
||||
this.store();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for partly matched results
|
||||
*
|
||||
* @param {string} type of the history record
|
||||
* @param {string} metaName name of the meta data
|
||||
* @param {string} keyword keyword to search
|
||||
* @param {number} max max results
|
||||
*/
|
||||
search(type, metaName, keyword, max) {
|
||||
let maxResults = max > this.records.length ? this.records.length : max;
|
||||
let s = [];
|
||||
|
||||
if (maxResults < 0) {
|
||||
maxResults = this.records.length;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.records.length && s.length < maxResults; i++) {
|
||||
if (this.records[i].type !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.records[i].data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!metaContains(this.records[i].data, metaName, keyword)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
s.push(this.records[i]);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
292
ui/commands/presets.js
Normal file
292
ui/commands/presets.js
Normal file
@@ -0,0 +1,292 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2020 Rui NI <nirui@gmx.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 Exception from "./exception.js";
|
||||
|
||||
/**
|
||||
* Default preset item, contains data of a default preset
|
||||
*
|
||||
*/
|
||||
const presetItem = {
|
||||
title: "",
|
||||
type: "",
|
||||
host: "",
|
||||
meta: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify Preset Item Meta
|
||||
*
|
||||
* @param {object} preset
|
||||
*
|
||||
*/
|
||||
function verifyPresetItemMeta(preset) {
|
||||
for (let i in preset.meta) {
|
||||
if (typeof preset.meta[i] === "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'The data type of meta field "' +
|
||||
i +
|
||||
'" was "' +
|
||||
typeof preset.meta[i] +
|
||||
'" instead of expected "string"'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and verify the given preset, return a valid preset
|
||||
*
|
||||
* @param {object} item
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
* @return {object}
|
||||
*
|
||||
*/
|
||||
function parsePresetItem(item) {
|
||||
let preset = {};
|
||||
|
||||
for (let i in presetItem) {
|
||||
preset[i] = presetItem[i];
|
||||
}
|
||||
|
||||
for (let i in presetItem) {
|
||||
if (typeof presetItem[i] === typeof item[i]) {
|
||||
preset[i] = item[i];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'Expecting the data type of "' +
|
||||
i +
|
||||
'" is "' +
|
||||
typeof presetItem[i] +
|
||||
'", given "' +
|
||||
typeof item[i] +
|
||||
'" instead'
|
||||
);
|
||||
}
|
||||
|
||||
verifyPresetItemMeta(preset.meta);
|
||||
|
||||
return preset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset data
|
||||
*
|
||||
*/
|
||||
export class Preset {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} preset preset data
|
||||
*
|
||||
*/
|
||||
constructor(preset) {
|
||||
this.preset = parsePresetItem(preset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the title of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
title() {
|
||||
return this.preset.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
type() {
|
||||
return this.preset.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the host of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
host() {
|
||||
return this.preset.host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the given meta of current preset
|
||||
*
|
||||
* @param {string} name name of the meta data
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
meta(name) {
|
||||
if (typeof this.preset.meta[name] !== "string") {
|
||||
throw new Exception('Meta "' + name + '" was undefined');
|
||||
}
|
||||
|
||||
return this.preset.meta[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new meta item
|
||||
*
|
||||
* @param {string} name name of the meta data
|
||||
* @param {string} data data of the meta data
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
*/
|
||||
insertMeta(name, data) {
|
||||
if (typeof this.preset.meta[name] !== "undefined") {
|
||||
throw new Exception('Meta "' + name + '" has already been defined');
|
||||
}
|
||||
|
||||
this.preset.meta[name] = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty preset
|
||||
*
|
||||
* @returns {Preset}
|
||||
*
|
||||
*/
|
||||
export function emptyPreset() {
|
||||
return new Preset({
|
||||
title: "Default",
|
||||
type: "Default",
|
||||
host: "",
|
||||
meta: {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Command Preset manager
|
||||
*
|
||||
*/
|
||||
export class Presets {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Array<object>} presets Array of preset data
|
||||
*
|
||||
*/
|
||||
constructor(presets) {
|
||||
this.presets = [];
|
||||
|
||||
for (let i = 0; i < presets.length; i++) {
|
||||
this.presets.push(new Preset(presets[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all presets of a type
|
||||
*
|
||||
* @param {string} type type of the presets data
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
fetch(type) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return presets with matched type and meta data
|
||||
*
|
||||
* @param {string} type type of the presets data
|
||||
* @param {string} metaName name of the meta data
|
||||
* @param {string} metaVal value of the meta data
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
meta(type, metaName, metaVal) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.presets[i].meta(metaName) !== metaVal) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof Exception)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return presets with matched type and host
|
||||
*
|
||||
* @param {string} type type of the presets
|
||||
* @param {string} host host of the presets
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
hosts(type, host) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.presets[i].host() !== host) {
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,18 @@
|
||||
// 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 header from "../stream/header.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as address from "./address.js";
|
||||
import * as command from "./commands.js";
|
||||
import * as common from "./common.js";
|
||||
import * as event from "./events.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as controls from "./controls.js";
|
||||
import * as header from "../stream/header.js";
|
||||
import * as history from "./history.js";
|
||||
import * as strings from "./string.js";
|
||||
import * as event from "./events.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as history from "./history.js";
|
||||
import * as presets from "./presets.js";
|
||||
import * as strings from "./string.js";
|
||||
|
||||
const AUTHMETHOD_NONE = 0x00;
|
||||
const AUTHMETHOD_PASSPHRASE = 0x01;
|
||||
@@ -57,6 +58,8 @@ const FingerprintPromptVerifyPassed = 0x00;
|
||||
const FingerprintPromptVerifyNoRecord = 0x01;
|
||||
const FingerprintPromptVerifyMismatch = 0x02;
|
||||
|
||||
const HostMaxSearchResults = 3;
|
||||
|
||||
class SSH {
|
||||
/**
|
||||
* constructor
|
||||
@@ -245,6 +248,10 @@ const initialFieldDef = {
|
||||
type: "text",
|
||||
value: "",
|
||||
example: "guest",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Username must be specified");
|
||||
@@ -265,6 +272,10 @@ const initialFieldDef = {
|
||||
type: "text",
|
||||
value: "",
|
||||
example: "ssh.vaguly.com:22",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Hostname must be specified");
|
||||
@@ -295,6 +306,10 @@ const initialFieldDef = {
|
||||
type: "select",
|
||||
value: "utf-8",
|
||||
example: common.charsetPresets.join(","),
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
for (let i in common.charsetPresets) {
|
||||
if (common.charsetPresets[i] !== d) {
|
||||
@@ -315,6 +330,10 @@ const initialFieldDef = {
|
||||
"SSH session is handled by the backend. Traffic will be decrypted " +
|
||||
"on the backend server and then transmit back to your client.",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
return "";
|
||||
}
|
||||
@@ -325,6 +344,10 @@ const initialFieldDef = {
|
||||
type: "password",
|
||||
value: "",
|
||||
example: "----------",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Password must be specified");
|
||||
@@ -356,6 +379,10 @@ const initialFieldDef = {
|
||||
type: "textfile",
|
||||
value: "",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Private Key must be specified");
|
||||
@@ -410,6 +437,10 @@ const initialFieldDef = {
|
||||
type: "radio",
|
||||
value: "",
|
||||
example: "Password,Private Key,None",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
switch (d) {
|
||||
case "Password":
|
||||
@@ -431,6 +462,10 @@ const initialFieldDef = {
|
||||
type: "textdata",
|
||||
value: "",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
return "";
|
||||
}
|
||||
@@ -468,7 +503,7 @@ class Wizard {
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {object} config
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
@@ -476,11 +511,11 @@ class Wizard {
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
constructor(info, preset, session, streams, subs, controls, history) {
|
||||
this.info = info;
|
||||
this.preset = preset;
|
||||
this.hasStarted = false;
|
||||
this.streams = streams;
|
||||
this.config = config;
|
||||
this.session = session
|
||||
? session
|
||||
: {
|
||||
@@ -489,7 +524,9 @@ class Wizard {
|
||||
this.step = subs;
|
||||
this.controls = controls.get("SSH");
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.step.resolve(this.stepInitialPrompt());
|
||||
}
|
||||
|
||||
@@ -680,26 +717,6 @@ class Wizard {
|
||||
stepInitialPrompt() {
|
||||
let self = this;
|
||||
|
||||
if (this.config) {
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
user: this.config.user,
|
||||
authentication: this.config.authentication,
|
||||
host: this.config.host,
|
||||
charset: this.config.charset ? this.config.charset : "utf-8",
|
||||
fingerprint: this.config.fingerprint
|
||||
},
|
||||
this.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
|
||||
return command.prompt(
|
||||
"SSH",
|
||||
"Secure Shell Host",
|
||||
@@ -717,26 +734,57 @@ class Wizard {
|
||||
charset: r.encoding,
|
||||
fingerprint: ""
|
||||
},
|
||||
this.session
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
self.step.resolve(self.stepWaitForAcceptWait());
|
||||
},
|
||||
() => {},
|
||||
command.fields(initialFieldDef, [
|
||||
{ name: "User" },
|
||||
{ name: "Host" },
|
||||
{ name: "Authentication" },
|
||||
{ name: "Encoding" },
|
||||
{ name: "Notice" }
|
||||
])
|
||||
command.fieldsWithPreset(
|
||||
initialFieldDef,
|
||||
[
|
||||
{ name: "User" },
|
||||
{
|
||||
name: "Host",
|
||||
suggestions(input) {
|
||||
const hosts = self.history.search(
|
||||
"SSH",
|
||||
"host",
|
||||
input,
|
||||
HostMaxSearchResults
|
||||
);
|
||||
|
||||
let sugg = [];
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
sugg.push({
|
||||
title: hosts[i].title,
|
||||
value: hosts[i].data.host,
|
||||
meta: {
|
||||
User: hosts[i].data.user,
|
||||
Authentication: hosts[i].data.authentication,
|
||||
Encoding: hosts[i].data.charset
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sugg;
|
||||
}
|
||||
},
|
||||
{ name: "Authentication" },
|
||||
{ name: "Encoding" },
|
||||
{ name: "Notice" }
|
||||
],
|
||||
self.preset
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async stepFingerprintPrompt(rd, sd, verify, newFingerprint) {
|
||||
let self = this,
|
||||
fingerprintData = new TextDecoder("utf-8").decode(
|
||||
const self = this;
|
||||
|
||||
let fingerprintData = new TextDecoder("utf-8").decode(
|
||||
await reader.readCompletely(rd)
|
||||
),
|
||||
fingerprintChanged = false;
|
||||
@@ -745,7 +793,7 @@ class Wizard {
|
||||
case FingerprintPromptVerifyPassed:
|
||||
sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([0]));
|
||||
|
||||
return this.stepContinueWaitForEstablishWait();
|
||||
return self.stepContinueWaitForEstablishWait();
|
||||
|
||||
case FingerprintPromptVerifyMismatch:
|
||||
fingerprintChanged = true;
|
||||
@@ -783,8 +831,9 @@ class Wizard {
|
||||
}
|
||||
|
||||
async stepCredentialPrompt(rd, sd, config, newCredential) {
|
||||
let self = this,
|
||||
fields = [];
|
||||
const self = this;
|
||||
|
||||
let fields = [];
|
||||
|
||||
if (config.credential.length > 0) {
|
||||
sd.send(
|
||||
@@ -792,7 +841,7 @@ class Wizard {
|
||||
new TextEncoder().encode(config.credential)
|
||||
);
|
||||
|
||||
return this.stepContinueWaitForEstablishWait();
|
||||
return self.stepContinueWaitForEstablishWait();
|
||||
}
|
||||
|
||||
switch (config.auth) {
|
||||
@@ -836,11 +885,61 @@ class Wizard {
|
||||
)
|
||||
);
|
||||
},
|
||||
command.fields(initialFieldDef, fields)
|
||||
command.fieldsWithPreset(initialFieldDef, fields, self.preset)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Executer extends Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {config} config
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
super(
|
||||
info,
|
||||
presets.emptyPreset(),
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
stepInitialPrompt() {
|
||||
const self = this;
|
||||
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
user: self.config.user,
|
||||
authentication: self.config.authentication,
|
||||
host: self.config.host,
|
||||
charset: self.config.charset ? self.config.charset : "utf-8",
|
||||
fingerprint: self.config.fingerprint
|
||||
},
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
}
|
||||
|
||||
export class Command {
|
||||
constructor() {}
|
||||
|
||||
@@ -860,8 +959,20 @@ export class Command {
|
||||
return "#3c8";
|
||||
}
|
||||
|
||||
builder(info, config, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, config, session, streams, subs, controls, history);
|
||||
wizard(info, preset, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, preset, session, streams, subs, controls, history);
|
||||
}
|
||||
|
||||
execute(info, config, session, streams, subs, controls, history) {
|
||||
return new Executer(
|
||||
info,
|
||||
config,
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
}
|
||||
|
||||
launch(info, launcher, streams, subs, controls, history) {
|
||||
@@ -893,7 +1004,7 @@ export class Command {
|
||||
);
|
||||
}
|
||||
|
||||
return this.builder(
|
||||
return this.execute(
|
||||
info,
|
||||
{
|
||||
user: user,
|
||||
@@ -920,4 +1031,14 @@ export class Command {
|
||||
(config.charset ? config.charset : "utf-8")
|
||||
);
|
||||
}
|
||||
|
||||
represet(preset) {
|
||||
const host = preset.host();
|
||||
|
||||
if (host.length > 0) {
|
||||
preset.insertMeta("Host", host);
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,17 @@
|
||||
// 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 header from "../stream/header.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as address from "./address.js";
|
||||
import * as command from "./commands.js";
|
||||
import * as common from "./common.js";
|
||||
import * as event from "./events.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as controls from "./controls.js";
|
||||
import * as history from "./history.js";
|
||||
import * as header from "../stream/header.js";
|
||||
import * as event from "./events.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as history from "./history.js";
|
||||
import * as presets from "./presets.js";
|
||||
|
||||
const COMMAND_ID = 0x00;
|
||||
|
||||
@@ -36,6 +37,8 @@ const SERVER_DIAL_CONNECTED = 0x02;
|
||||
|
||||
const DEFAULT_PORT = 23;
|
||||
|
||||
const HostMaxSearchResults = 3;
|
||||
|
||||
class Telnet {
|
||||
/**
|
||||
* constructor
|
||||
@@ -185,6 +188,10 @@ const initialFieldDef = {
|
||||
type: "text",
|
||||
value: "",
|
||||
example: "telnet.vaguly.com:23",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Hostname must be specified");
|
||||
@@ -215,6 +222,10 @@ const initialFieldDef = {
|
||||
type: "select",
|
||||
value: "utf-8",
|
||||
example: common.charsetPresets.join(","),
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
for (let i in common.charsetPresets) {
|
||||
if (common.charsetPresets[i] !== d) {
|
||||
@@ -234,7 +245,7 @@ class Wizard {
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {object} config
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
@@ -242,16 +253,18 @@ class Wizard {
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
constructor(info, preset, session, streams, subs, controls, history) {
|
||||
this.info = info;
|
||||
this.preset = preset;
|
||||
this.hasStarted = false;
|
||||
this.streams = streams;
|
||||
this.config = config;
|
||||
this.session = session;
|
||||
this.step = subs;
|
||||
this.controls = controls.get("Telnet");
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.step.resolve(this.stepInitialPrompt());
|
||||
}
|
||||
|
||||
@@ -378,24 +391,7 @@ class Wizard {
|
||||
}
|
||||
|
||||
stepInitialPrompt() {
|
||||
let self = this;
|
||||
|
||||
if (this.config) {
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
host: this.config.host,
|
||||
charset: this.config.charset ? this.config.charset : "utf-8"
|
||||
},
|
||||
this.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
const self = this;
|
||||
|
||||
return command.prompt(
|
||||
"Telnet",
|
||||
@@ -411,18 +407,96 @@ class Wizard {
|
||||
host: r.host,
|
||||
charset: r.encoding
|
||||
},
|
||||
this.session
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
self.step.resolve(self.stepWaitForAcceptWait());
|
||||
},
|
||||
() => {},
|
||||
command.fields(initialFieldDef, [{ name: "Host" }, { name: "Encoding" }])
|
||||
command.fieldsWithPreset(
|
||||
initialFieldDef,
|
||||
[
|
||||
{
|
||||
name: "Host",
|
||||
suggestions(input) {
|
||||
const hosts = self.history.search(
|
||||
"Telnet",
|
||||
"host",
|
||||
input,
|
||||
HostMaxSearchResults
|
||||
);
|
||||
|
||||
let sugg = [];
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
sugg.push({
|
||||
title: hosts[i].title,
|
||||
value: hosts[i].data.host,
|
||||
meta: {
|
||||
Encoding: hosts[i].data.charset
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sugg;
|
||||
}
|
||||
},
|
||||
{ name: "Encoding" }
|
||||
],
|
||||
self.preset
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Executor extends Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {object} config
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
super(
|
||||
info,
|
||||
presets.emptyPreset(),
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
stepInitialPrompt() {
|
||||
const self = this;
|
||||
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
host: self.config.host,
|
||||
charset: self.config.charset ? self.config.charset : "utf-8"
|
||||
},
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
}
|
||||
|
||||
export class Command {
|
||||
constructor() {}
|
||||
|
||||
@@ -442,8 +516,20 @@ export class Command {
|
||||
return "#6ac";
|
||||
}
|
||||
|
||||
builder(info, config, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, config, session, streams, subs, controls, history);
|
||||
wizard(info, preset, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, preset, session, streams, subs, controls, history);
|
||||
}
|
||||
|
||||
execute(info, config, session, streams, subs, controls, history) {
|
||||
return new Executor(
|
||||
info,
|
||||
config,
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
}
|
||||
|
||||
launch(info, launcher, streams, subs, controls, history) {
|
||||
@@ -476,7 +562,7 @@ export class Command {
|
||||
}
|
||||
}
|
||||
|
||||
return this.builder(
|
||||
return this.execute(
|
||||
info,
|
||||
{
|
||||
host: d[0],
|
||||
@@ -493,4 +579,14 @@ export class Command {
|
||||
launcher(config) {
|
||||
return config.host + "|" + (config.charset ? config.charset : "utf-8");
|
||||
}
|
||||
|
||||
represet(preset) {
|
||||
const host = preset.host();
|
||||
|
||||
if (host.length > 0) {
|
||||
preset.insertMeta("Host", host);
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,28 @@ body {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.hlst.lstcl2 {
|
||||
list-style: none;
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.hlst.lstcl2 > li {
|
||||
width: 33%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hlst.lstcl2 > li .lst-wrap {
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
background: #333;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Icon */
|
||||
.icon {
|
||||
line-height: 1;
|
||||
|
||||
80
ui/home.vue
80
ui/home.vue
@@ -110,6 +110,8 @@
|
||||
: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"
|
||||
@@ -119,6 +121,7 @@
|
||||
@connector-select="connectNew"
|
||||
@known-select="connectKnown"
|
||||
@known-remove="removeKnown"
|
||||
@preset-select="connectPreset"
|
||||
@known-clear-session="clearSessionKnown"
|
||||
>
|
||||
<connector
|
||||
@@ -143,8 +146,9 @@
|
||||
@current="switchTab"
|
||||
@retap="retapTab"
|
||||
@close="closeTab"
|
||||
></tab-window></div
|
||||
></template>
|
||||
></tab-window>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./home.css";
|
||||
@@ -159,6 +163,8 @@ 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: ";
|
||||
@@ -198,6 +204,18 @@ export default {
|
||||
default: () => {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
presetData: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return new presets.Presets([]);
|
||||
}
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -220,6 +238,7 @@ export default {
|
||||
busy: false,
|
||||
knowns: history.all()
|
||||
},
|
||||
presets: this.commands.mergePresets(this.presetData),
|
||||
tab: {
|
||||
current: -1,
|
||||
lastID: 0,
|
||||
@@ -317,22 +336,45 @@ export default {
|
||||
);
|
||||
},
|
||||
connectNew(connector) {
|
||||
this.runConnect(stream => {
|
||||
this.connector.connector = {
|
||||
const self = this;
|
||||
|
||||
self.runConnect(stream => {
|
||||
self.connector.connector = {
|
||||
id: connector.id(),
|
||||
name: connector.name(),
|
||||
description: connector.description(),
|
||||
wizard: connector.build(
|
||||
wizard: connector.wizard(
|
||||
stream,
|
||||
this.controls,
|
||||
this.connector.historyRec,
|
||||
null,
|
||||
self.controls,
|
||||
self.connector.historyRec,
|
||||
presets.emptyPreset(),
|
||||
null,
|
||||
() => {}
|
||||
)
|
||||
};
|
||||
|
||||
this.connector.inputting = true;
|
||||
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) {
|
||||
@@ -349,27 +391,27 @@ export default {
|
||||
return connector;
|
||||
},
|
||||
connectKnown(known) {
|
||||
this.runConnect(stream => {
|
||||
let connector = this.getConnectorByType(known.type);
|
||||
const self = this;
|
||||
|
||||
self.runConnect(stream => {
|
||||
let connector = self.getConnectorByType(known.type);
|
||||
|
||||
if (!connector) {
|
||||
alert("Unknown connector: " + known.type);
|
||||
|
||||
this.connector.inputting = false;
|
||||
self.connector.inputting = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
this.connector.connector = {
|
||||
self.connector.connector = {
|
||||
id: connector.id(),
|
||||
name: connector.name(),
|
||||
description: connector.description(),
|
||||
wizard: connector.build(
|
||||
wizard: connector.execute(
|
||||
stream,
|
||||
this.controls,
|
||||
this.connector.historyRec,
|
||||
self.controls,
|
||||
self.connector.historyRec,
|
||||
known.data,
|
||||
known.session,
|
||||
() => {
|
||||
@@ -378,7 +420,7 @@ export default {
|
||||
)
|
||||
};
|
||||
|
||||
this.connector.inputting = true;
|
||||
self.connector.inputting = true;
|
||||
});
|
||||
},
|
||||
parseConnectLauncher(ll) {
|
||||
|
||||
@@ -44,11 +44,14 @@
|
||||
|
||||
<connect-known
|
||||
v-if="tab === 'known' && !inputting"
|
||||
:presets="presets"
|
||||
:restricted-to-presets="restrictedToPresets"
|
||||
:knowns="knowns"
|
||||
:launcher-builder="knownsLauncherBuilder"
|
||||
:knowns-export="knownsExport"
|
||||
:knowns-import="knownsImport"
|
||||
@select="selectKnown"
|
||||
@select-preset="selectPreset"
|
||||
@remove="removeKnown"
|
||||
@clear-session="clearSessionKnown"
|
||||
></connect-known>
|
||||
@@ -100,6 +103,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
presets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -160,6 +171,13 @@ export default {
|
||||
|
||||
this.$emit("known-remove", uid);
|
||||
},
|
||||
selectPreset(preset) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("preset-select", preset);
|
||||
},
|
||||
clearSessionKnown(uid) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
|
||||
@@ -22,13 +22,20 @@
|
||||
#connect-known-list {
|
||||
min-height: 200px;
|
||||
font-size: 0.75em;
|
||||
padding: 15px;
|
||||
background: #3a3a3a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connect-known-list h3 {
|
||||
font-size: 1.1em;
|
||||
color: #999;
|
||||
margin: 5px 0 15px 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#connect-known-list.reloaded {
|
||||
}
|
||||
|
||||
@@ -62,10 +69,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-import {
|
||||
margin: 15px 0 10px 0;
|
||||
margin: 15px 0;
|
||||
color: #aaa;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
@@ -76,26 +84,30 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#connect-known-list li {
|
||||
#connect-known-list-list {
|
||||
padding: 15px 20px 20px 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-list li {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list li {
|
||||
#connect-known-list-list li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list li .lst-wrap {
|
||||
#connect-known-list-list li > .lst-wrap {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#connect-known-list li .lst-wrap:hover {
|
||||
#connect-known-list-list li > .lst-wrap:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels {
|
||||
#connect-known-list-list li > .labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -104,14 +116,14 @@
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .type {
|
||||
#connect-known-list-list li > .labels > .type {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt {
|
||||
#connect-known-list-list li > .labels > .opt {
|
||||
display: none;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
@@ -121,43 +133,44 @@
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list li .labels > .opt {
|
||||
#connect-known-list-list li > .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.link {
|
||||
#connect-known-list-list li > .labels > .opt.link {
|
||||
background: #287;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.link:after {
|
||||
#connect-known-list-list li > .labels > .opt.link:after {
|
||||
content: "\02936";
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.del {
|
||||
#connect-known-list-list li > .labels > .opt.del {
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.clr {
|
||||
#connect-known-list-list li > .labels > .opt.clr {
|
||||
background: #b71;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li:hover .labels > .opt,
|
||||
#connect-known-list li:focus .labels > .opt {
|
||||
#connect-known-list-list li:hover > .labels > .opt,
|
||||
#connect-known-list-list li:focus > .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#connect-known-list li h2 {
|
||||
#connect-known-list-list li > .lst-wrap > h4 {
|
||||
font-size: 1.5em;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#connect-known-list li h2::before {
|
||||
#connect-known-list-list li > .lst-wrap > h4::before {
|
||||
content: ">_";
|
||||
color: #555;
|
||||
font-size: 0.8em;
|
||||
@@ -167,7 +180,70 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#connect-known-list li h2.highlight::before {
|
||||
#connect-known-list-list li > .lst-wrap > h4.highlight::before {
|
||||
color: #eee;
|
||||
background: #555;
|
||||
}
|
||||
|
||||
#connect-known-list-presets {
|
||||
margin-top: 10px;
|
||||
padding: 15px 20px 20px 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets.last-planel {
|
||||
background: #3f3f3f;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list-presets li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap {
|
||||
cursor: pointer;
|
||||
border-radius: 0 3px 3px 3px;
|
||||
margin: 12px 10px 10px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > .labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > .labels > .type {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
border-radius: 3px 3px 3px 0;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > h4 {
|
||||
font-size: 1.3em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#connect-known-list-presets-alert {
|
||||
font-size: 1.15em;
|
||||
color: #fff;
|
||||
background: #c73;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -19,53 +19,104 @@
|
||||
|
||||
<template>
|
||||
<div id="connect-known-list" :class="{ reloaded: reloaded }">
|
||||
<div v-if="knownList.length <= 0" id="connect-known-list-empty">
|
||||
<div
|
||||
v-if="knownList.length <= 0 && presets <= 0"
|
||||
id="connect-known-list-empty"
|
||||
>
|
||||
No known remote available
|
||||
</div>
|
||||
<ul v-else id="connect-known-list-list" class="hlst lstcl1">
|
||||
<li v-for="(known, kk) in knownList" :key="kk">
|
||||
<div class="labels">
|
||||
<span class="type" :style="'background-color: ' + known.data.color">
|
||||
{{ known.data.type }}
|
||||
</span>
|
||||
<div v-else>
|
||||
<div v-if="knownList.length > 0" id="connect-known-list-list">
|
||||
<h3>Connected before</h3>
|
||||
|
||||
<a
|
||||
class="opt link"
|
||||
href="javascript:;"
|
||||
@click="launcher(known, $event)"
|
||||
>
|
||||
{{ known.copyStatus }}
|
||||
</a>
|
||||
<ul class="hlst lstcl1">
|
||||
<li v-for="(known, kk) in knownList" :key="kk">
|
||||
<div class="labels">
|
||||
<span
|
||||
class="type"
|
||||
:style="'background-color: ' + known.data.color"
|
||||
>
|
||||
{{ known.data.type }}
|
||||
</span>
|
||||
|
||||
<a
|
||||
v-if="!known.data.session"
|
||||
class="opt del"
|
||||
href="javascript:;"
|
||||
@click="remove(known.data.uid)"
|
||||
<a
|
||||
class="opt link"
|
||||
href="javascript:;"
|
||||
@click="launcher(known, $event)"
|
||||
>
|
||||
{{ known.copyStatus }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="!known.data.session"
|
||||
class="opt del"
|
||||
href="javascript:;"
|
||||
@click="remove(known.data.uid)"
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="opt clr"
|
||||
href="javascript:;"
|
||||
title="Clear session data"
|
||||
@click="clearSession(known.data.uid)"
|
||||
>
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="lst-wrap" @click="select(known.data)">
|
||||
<h4
|
||||
:title="known.data.title"
|
||||
:class="{ highlight: known.data.session }"
|
||||
>
|
||||
{{ known.data.title }}
|
||||
</h4>
|
||||
Last: {{ known.data.last.toLocaleString() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="presets.length > 0"
|
||||
id="connect-known-list-presets"
|
||||
:class="{
|
||||
'last-planel': knownList.length > 0
|
||||
}"
|
||||
>
|
||||
<h3>Presets</h3>
|
||||
|
||||
<ul class="hlst lstcl2">
|
||||
<li
|
||||
v-for="(preset, pk) in presets"
|
||||
:key="pk"
|
||||
:class="{ disabled: presetDisabled(preset) }"
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="opt clr"
|
||||
href="javascript:;"
|
||||
title="Clear session data"
|
||||
@click="clearSession(known.data.uid)"
|
||||
>
|
||||
Clear
|
||||
</a>
|
||||
<div class="lst-wrap" @click="selectPreset(preset)">
|
||||
<div class="labels">
|
||||
<span
|
||||
class="type"
|
||||
:style="'background-color: ' + preset.command.color()"
|
||||
>
|
||||
{{ preset.command.name() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 :title="preset.preset.title()">
|
||||
{{ preset.preset.title() }}
|
||||
</h4>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="restrictedToPresets" id="connect-known-list-presets-alert">
|
||||
The operator has disabled outgoing access to all remote hosts except
|
||||
those been defined as preset.
|
||||
</div>
|
||||
<div class="lst-wrap" @click="select(known.data)">
|
||||
<h2
|
||||
:title="known.data.title"
|
||||
:class="{ highlight: known.data.session }"
|
||||
>
|
||||
{{ known.data.title }}
|
||||
</h2>
|
||||
Last: {{ known.data.last.toLocaleString() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="connect-known-list-import">
|
||||
Tip: You can
|
||||
@@ -81,6 +132,14 @@ import "./connect_known.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
presets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -147,6 +206,20 @@ export default {
|
||||
|
||||
this.$emit("select", known);
|
||||
},
|
||||
presetDisabled(preset) {
|
||||
if (!this.restrictedToPresets || preset.preset.host().length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
selectPreset(preset) {
|
||||
if (this.busy || this.presetDisabled(preset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("select-preset", preset);
|
||||
},
|
||||
async launcher(known, ev) {
|
||||
if (known.copying || this.busy) {
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// 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/>.
|
||||
|
||||
export function head(url, headers) {
|
||||
export function get(url, headers) {
|
||||
return new Promise((res, rej) => {
|
||||
let authReq = new XMLHttpRequest();
|
||||
|
||||
@@ -35,7 +35,7 @@ export function head(url, headers) {
|
||||
rej(e);
|
||||
};
|
||||
|
||||
authReq.open("HEAD", url, true);
|
||||
authReq.open("GET", url, true);
|
||||
|
||||
for (let h in headers) {
|
||||
authReq.setRequestHeader(h, headers[h]);
|
||||
|
||||
Reference in New Issue
Block a user