Implemented the host name auto suggestion, and added Preset feature

This commit is contained in:
NI
2020-02-07 18:05:44 +08:00
parent 0a930d1345
commit 67c99e3092
22 changed files with 1582 additions and 332 deletions

116
ui/app.js
View File

@@ -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:

View File

@@ -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;
}
}

View File

@@ -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
View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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]);