Initial commit

This commit is contained in:
NI
2019-08-07 15:56:51 +08:00
commit 02f14eb14f
206 changed files with 38863 additions and 0 deletions

228
ui/commands/address.js Normal file
View File

@@ -0,0 +1,228 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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";
import * as reader from "../stream/reader.js";
import * as common from "./common.js";
export const LOOPBACK = 0x00;
export const IPV4 = 0x01;
export const IPV6 = 0x02;
export const HOSTNAME = 0x03;
export const MAX_ADDR_LEN = 0x3f;
export class Address {
/**
* Read builds an Address from data readed from the reader
*
* @param {reader.Reader} rd The reader
*
* @returns {Address} The Address
*
* @throws {Exception} when address type is invalid
*/
static async read(rd) {
let readed = await reader.readN(rd, 3),
portNum = 0,
addrType = LOOPBACK,
addrData = null;
portNum |= readed[0];
portNum <<= 8;
portNum |= readed[1];
addrType = readed[2] >> 6;
switch (addrType) {
case LOOPBACK:
break;
case IPV4:
addrData = await reader.readN(rd, 4);
break;
case IPV6:
addrData = await reader.readN(rd, 16);
break;
case HOSTNAME:
addrData = await reader.readN(rd, 0x3f & readed[2]);
break;
default:
throw new Exception("Unknown address type");
}
return new Address(addrType, addrData, portNum);
}
/**
* constructor
*
* @param {number} type Type of the address
* @param {Uint8Array} address Address data
* @param {number} port port number of the address
*
*/
constructor(type, address, port) {
this.addrType = type;
this.addrData = address;
this.addrPort = port;
}
/**
* Return the address type
*
*/
type() {
return this.addrType;
}
/**
* Return the address data
*
*/
address() {
return this.addrData;
}
/**
* Return the port data
*
*/
port() {
return this.addrPort;
}
/**
* Buffer returns the marshalled address
*
* @returns {Uint8Array} Marshalled address
*
* @throws {Exception} When address data is invalid
*
*/
buffer() {
switch (this.type()) {
case LOOPBACK:
return new Uint8Array([
this.addrPort >> 8,
this.addrPort & 0xff,
LOOPBACK << 6
]);
case IPV4:
if (this.addrData.length != 4) {
throw new Exception("Invalid address length");
}
return new Uint8Array([
this.addrPort >> 8,
this.addrPort & 0xff,
IPV4 << 6,
this.addrData[0],
this.addrData[1],
this.addrData[2],
this.addrData[3]
]);
case IPV6:
if (this.addrData.length != 16) {
throw new Exception("Invalid address length");
}
return new Uint8Array([
this.addrPort >> 8,
this.addrPort & 0xff,
IPV6 << 6,
this.addrData[0],
this.addrData[1],
this.addrData[2],
this.addrData[3],
this.addrData[4],
this.addrData[5],
this.addrData[6],
this.addrData[7],
this.addrData[8],
this.addrData[9],
this.addrData[10],
this.addrData[11],
this.addrData[12],
this.addrData[13],
this.addrData[14],
this.addrData[15]
]);
case HOSTNAME:
if (this.addrData.length > MAX_ADDR_LEN) {
throw new Exception("Host name cannot longer than " + MAX_ADDR_LEN);
}
let dataBuf = new Uint8Array(this.addrData.length + 3);
dataBuf[0] = (this.addrPort >> 8) & 0xff;
dataBuf[1] = this.addrPort & 0xff;
dataBuf[2] = HOSTNAME << 6;
dataBuf[2] |= this.addrData.length;
dataBuf.set(this.addrData, 3);
return dataBuf;
default:
throw new Exception("Unknown address type");
}
}
}
/**
* Get address data
*
* @param {string} s Address string
* @param {number} defaultPort Default port number
*
* @returns {object} result
*
* @throws {Exception} when the address is invalid
*/
export function parseHostPort(s, defaultPort) {
let d = common.splitHostPort(s, defaultPort),
t = HOSTNAME;
switch (d.type) {
case "IPv4":
t = IPV4;
break;
case "IPv6":
t = IPV6;
break;
case "Hostname":
break;
default:
throw new Exception("Invalid address type");
}
return {
type: t,
address: d.addr,
port: d.port
};
}

102
ui/commands/address_test.js Normal file
View File

@@ -0,0 +1,102 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 assert from "assert";
import * as reader from "../stream/reader.js";
import * as address from "./address.js";
describe("Address", () => {
it("Address Loopback", async () => {
let addr = new address.Address(address.LOOPBACK, null, 8080),
buf = addr.buffer();
let r = new reader.Reader(new reader.Multiple(), data => {
return data;
});
r.feed(buf);
let addr2 = await address.Address.read(r);
assert.equal(addr2.type(), addr.type());
assert.deepEqual(addr2.address(), addr.address());
assert.equal(addr2.port(), addr.port());
});
it("Address IPv4", async () => {
let addr = new address.Address(
address.IPV4,
new Uint8Array([127, 0, 0, 1]),
8080
),
buf = addr.buffer();
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
r.feed(buf);
let addr2 = await address.Address.read(r);
assert.equal(addr2.type(), addr.type());
assert.deepEqual(addr2.address(), addr.address());
assert.equal(addr2.port(), addr.port());
});
it("Address IPv6", async () => {
let addr = new address.Address(
address.IPV6,
new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
8080
),
buf = addr.buffer();
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
r.feed(buf);
let addr2 = await address.Address.read(r);
assert.equal(addr2.type(), addr.type());
assert.deepEqual(addr2.address(), addr.address());
assert.equal(addr2.port(), addr.port());
});
it("Address HostName", async () => {
let addr = new address.Address(
address.HOSTNAME,
new Uint8Array(["v", "a", "g", "u", "l", "1", "2", "3"]),
8080
),
buf = addr.buffer();
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
r.feed(buf);
let addr2 = await address.Address.read(r);
assert.equal(addr2.type(), addr.type());
assert.deepEqual(addr2.address(), addr.address());
assert.equal(addr2.port(), addr.port());
});
});

107
ui/commands/color.js Normal file
View File

@@ -0,0 +1,107 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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/>.
/**
* Get one color hex byte
*
* @param {number} from Min color number
* @param {number} to Max color number
*
* @returns {string} color byte in string
*
*/
function getRandHex(from, to) {
let color = Math.random() * (to - from) + from,
colorDark = color - color / 20;
let r = Math.round(color).toString(16),
rDark = Math.round(colorDark).toString(16);
if (r.length % 2 !== 0) {
r = "0" + r;
}
if (rDark.length % 2 !== 0) {
rDark = "0" + rDark;
}
return [r, rDark];
}
/**
* Get rand color
*
* @param {number} from Min color number
* @param {number} to Max color number
*
* @returns {string} Color bytes in string
*/
function getRandColor(from, to) {
let r = getRandHex(from, to),
g = getRandHex(from, to),
b = getRandHex(from, to);
return ["#" + r[0] + g[0] + b[0], "#" + r[1] + g[1] + b[1]];
}
export class Color {
/**
* constructor
*/
constructor() {
this.assignedColors = {};
}
/**
* Get one color
*
* @returns {string} Color code
*
*/
get() {
const maxTries = 10;
let tried = 0;
for (;;) {
let color = getRandColor(0x22, 0x33);
if (this.assignedColors[color[0]]) {
tried++;
if (tried < maxTries) {
continue;
}
}
this.assignedColors[color[0]] = true;
return {
color: color[0],
dark: color[1]
};
}
}
/**
* forget already assigned color
*
* @param {string} color Color code
*/
forget(color) {
delete this.assignedColors[color];
}
}

720
ui/commands/commands.js Normal file
View File

@@ -0,0 +1,720 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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";
import * as stream from "../stream/streams.js";
import * as subscribe from "../stream/subscribe.js";
export const NEXT_PROMPT = 1;
export const NEXT_WAIT = 2;
export const NEXT_DONE = 3;
export class Result {
/**
* constructor
*
* @param {string} name Result type
* @param {Info} info Result info
* @param {object} control Result controller
*/
constructor(name, info, control) {
this.name = name;
this.info = info;
this.control = control;
}
}
class Done {
/**
* constructor
*
* @param {object} data Step data
*
*/
constructor(data) {
this.s = !!data.success;
this.d = data.successData;
this.errorTitle = data.errorTitle;
this.errorMessage = data.errorMessage;
}
/**
* Return the error of current Done
*
* @returns {string} title
*
*/
error() {
return this.errorTitle;
}
/**
* Return the error message of current Done
*
* @returns {string} message
*
*/
message() {
return this.errorMessage;
}
/**
* Returns whether or not current Done is representing a success
*
* @returns {boolean} True when success, false otherwise
*/
success() {
return this.s;
}
/**
* Returns final data
*
* @returns {Result} Successful result
*/
data() {
return this.d;
}
}
class Wait {
/**
* constructor
*
* @param {object} data Step data
*
*/
constructor(data) {
this.t = data.title;
this.m = data.message;
}
/**
* Return the title of current Wait
*
* @returns {string} title
*
*/
title() {
return this.t;
}
/**
* Return the message of current Wait
*
* @returns {string} message
*
*/
message() {
return this.m;
}
}
const defField = {
name: "",
description: "",
type: "",
value: "",
example: "",
verify(v) {
return "OK";
}
};
/**
* Create a Prompt field
*
* @param {object} def Field default value
* @param {object} f Field value
*
* @returns {object} Field data
*
* @throws {Exception} When input field is invalid
*
*/
export function field(def, f) {
let n = {};
for (let i in def) {
n[i] = def[i];
}
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'
);
}
n[i] = f[i];
}
if (!n["name"]) {
throw new Exception('Field "name" must be specified');
}
return n;
}
/**
* Build a group of field value
*
* @param {object} definitions Definition of a group of fields
* @param {array<object>} fs Data of the field group
*
* @returns {array<object>} Result fields
*
* @throws {Exception} When input field is invalid
*
*/
export function fields(definitions, fs) {
let fss = [];
for (let i in fs) {
if (!fs[i]["name"]) {
throw new Exception('Field "name" must be specified');
}
if (!definitions[fs[i].name]) {
throw new Exception('Undefined field "' + fs[i].name + '"');
}
fss.push(field(definitions[fs[i].name], fs[i]));
}
return fss;
}
class Prompt {
/**
* constructor
*
* @param {object} data Step data
*
* @throws {Exception} If the field verify is not a function while
* not null
*/
constructor(data) {
this.t = data.title;
this.m = data.message;
this.a = data.actionText;
this.r = data.respond;
this.c = data.cancel;
this.i = [];
this.f = {};
for (let i in data.inputs) {
let f = field(defField, data.inputs[i]);
this.i.push(f);
this.f[data.inputs[i].name.toLowerCase()] = {
value: f.value,
verify: f.verify
};
}
}
/**
* Return the title of current Prompt
*
* @returns {string} title
*
*/
title() {
return this.t;
}
/**
* Return the message of current Prompt
*
* @returns {string} message
*
*/
message() {
return this.m;
}
/**
* Return the input field of current prompt
*
* @returns {array} Input fields
*
*/
inputs() {
let inputs = [];
for (let i in this.i) {
inputs.push(this.i[i]);
}
return inputs;
}
/**
* Returns the name of the action
*
* @returns {string} Action name
*
*/
actionText() {
return this.a;
}
/**
* Receive the submit of current prompt
*
* @param {object} inputs Input value
*
* @returns {any} The result of the step responder
*
* @throws {Exception} When the field is undefined or invalid
*
*/
submit(inputs) {
let fields = {};
for (let i in this.f) {
fields[i] = this.f[i].value;
}
for (let i in inputs) {
let k = i.toLowerCase();
if (typeof fields[k] === "undefined") {
throw new Exception('Field "' + k + '" is undefined');
}
try {
this.f[k].verify(inputs[i]);
} catch (e) {
throw new Exception('Field "' + k + '" is invalid: ' + e);
}
fields[k] = inputs[i];
}
return this.r(fields);
}
/**
* Cancel current wait operation
*
*/
cancel() {
return this.c();
}
}
/**
* Create a Wizard step
*
* @param {string} type Step type
* @param {object} data Step data
*
* @returns {object} Step data
*
*/
function next(type, data) {
return {
type() {
return type;
},
data() {
return data;
}
};
}
/**
* Create data for a Done step of the wizard
*
* @param {boolean} success
* @param {Success} successData
* @param {string} errorTitle
* @param {string} errorMessage
*
* @returns {object} Done step data
*
*/
export function done(success, successData, errorTitle, errorMessage) {
return next(NEXT_DONE, {
success: success,
successData: successData,
errorTitle: errorTitle,
errorMessage: errorMessage
});
}
/**
* Create data for a Wait step of the wizard
*
* @param {string} title Waiter title
* @param {message} message Waiter message
*
* @returns {object} Done step data
*
*/
export function wait(title, message) {
return next(NEXT_WAIT, {
title: title,
message: message
});
}
/**
* Create data for a Prompt step of the wizard
*
* @param {string} title Title of the prompt
* @param {string} message Message of the prompt
* @param {string} actionText Text of the action (button)
* @param {function} respond Respond callback
* @param {function} cancel cancel handler
* @param {object} inputs Input field objects
*
* @returns {object} Prompt step data
*
*/
export function prompt(title, message, actionText, respond, cancel, inputs) {
return next(NEXT_PROMPT, {
title: title,
message: message,
actionText: actionText,
inputs: inputs,
respond: respond,
cancel: cancel
});
}
class Next {
/**
* constructor
*
* @param {object} data Step data
*/
constructor(data) {
this.t = data.type();
this.d = data.data();
}
/**
* Return step type
*
* @returns {string} Step type
*/
type() {
return this.t;
}
/**
* Return step data
*
* @returns {Done|Prompt} Step data
*
* @throws {Exception} When the step type is unknown
*
*/
data() {
switch (this.type()) {
case NEXT_PROMPT:
return new Prompt(this.d);
case NEXT_WAIT:
return new Wait(this.d);
case NEXT_DONE:
return new Done(this.d);
default:
throw new Exception("Unknown data type");
}
}
}
class Wizard {
/**
* constructor
*
* @param {function} builder Command builder
* @param {subscribe.Subscribe} subs Wizard step subscriber
*
*/
constructor(built, subs) {
this.built = built;
this.subs = subs;
this.closed = false;
}
/**
* Return the Next step
*
* @returns {Next} Next step
*
* @throws {Exception} When wizard is closed
*
*/
async next() {
if (this.closed) {
throw new Exception("Wizard already closed, no next step is available");
}
let n = await this.subs.subscribe();
if (n.type() === NEXT_DONE) {
this.close();
}
return new Next(n);
}
/**
* Return whether or not the command is started
*
* @returns {boolean} True when the command already started, false otherwise
*
*/
started() {
return this.built.started();
}
/**
* Close current wizard
*
* @returns {any} Close result
*
*/
close() {
if (this.closed) {
return;
}
this.closed = true;
return this.built.close();
}
}
export class Info {
/**
* constructor
*
* @param {Builder} info Builder info
*
*/
constructor(info) {
this.type = info.name();
this.info = info.description();
this.tcolor = info.color();
}
/**
* Return command name
*
* @returns {string} Command name
*
*/
name() {
return this.type;
}
/**
* Return command description
*
* @returns {string} Command description
*
*/
description() {
return this.info;
}
/**
* Return the theme color of the command
*
* @returns {string} Command name
*
*/
color() {
return this.tcolor;
}
}
class Builder {
/**
* constructor
*
* @param {object} command Command builder
*
*/
constructor(command) {
this.cid = command.id();
this.builder = (n, i, r, u, y, x) => {
return command.builder(n, i, r, u, y, x);
};
this.launchCmd = (n, i, r, u, y, x) => {
return command.launch(n, i, r, u, y, x);
};
this.launcherCmd = c => {
return command.launcher(c);
};
this.type = command.name();
this.info = command.description();
this.tcolor = command.color();
}
/**
* Return the command ID
*
* @returns {number} Command ID
*
*/
id() {
return this.cid;
}
/**
* Return command name
*
* @returns {string} Command name
*
*/
name() {
return this.type;
}
/**
* Return command description
*
* @returns {string} Command description
*
*/
description() {
return this.info;
}
/**
* Return the theme color of the command
*
* @returns {string} Command name
*
*/
color() {
return this.tcolor;
}
/**
* Build command wizard
*
* @param {stream.Streams} streams
* @param {controls.Controls} controls
* @param {history.History} history
* @param {object} config
*
* @returns {Wizard} Command wizard
*
*/
build(streams, controls, history, config) {
let subs = new subscribe.Subscribe();
return new Wizard(
this.builder(new Info(this), config, streams, subs, controls, history),
subs
);
}
/**
* Launch command wizard out of given launcher string
*
* @param {stream.Streams} streams
* @param {controls.Controls} controls
* @param {history.History} history
* @param {string} launcher Launcher format
*
* @returns {Wizard} Command wizard
*
*/
launch(streams, controls, history, launcher) {
let subs = new subscribe.Subscribe();
return new Wizard(
this.launchCmd(
new Info(this),
launcher,
streams,
subs,
controls,
history
),
subs
);
}
/**
* Build launcher string out of given config
*
* @param {object} config Configuration object
*
* @return {string} Launcher string
*/
launcher(config) {
return this.name() + ":" + this.launcherCmd(config);
}
}
export class Commands {
/**
* constructor
*
* @param {[]object} commands Command array
*
*/
constructor(commands) {
this.commands = [];
for (let i in commands) {
this.commands.push(new Builder(commands[i]));
}
}
/**
* Return all commands
*
* @returns {[]Builder} A group of command
*
*/
all() {
return this.commands;
}
/**
* Select one command
*
* @param {number} id Command ID
*
* @returns {Builder} Command builder
*
*/
select(id) {
return this.commands[id];
}
}

360
ui/commands/common.js Normal file
View File

@@ -0,0 +1,360 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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";
const numCharators = {
"0": true,
"1": true,
"2": true,
"3": true,
"4": true,
"5": true,
"6": true,
"7": true,
"8": true,
"9": true
};
const hexCharators = {
"0": true,
"1": true,
"2": true,
"3": true,
"4": true,
"5": true,
"6": true,
"7": true,
"8": true,
"9": true,
a: true,
b: true,
c: true,
d: true,
e: true,
f: true
};
const hostnameCharators = {
"0": true,
"1": true,
"2": true,
"3": true,
"4": true,
"5": true,
"6": true,
"7": true,
"8": true,
"9": true,
a: true,
b: true,
c: true,
d: true,
e: true,
f: true,
g: true,
h: true,
i: true,
j: true,
k: true,
l: true,
n: true,
m: true,
o: true,
p: true,
q: true,
r: true,
s: true,
t: true,
u: true,
v: true,
w: true,
x: true,
y: true,
z: true,
".": true,
"-": true,
_: true
};
/**
* Test whether or not given string is all number
*
* @param {string} d Input data
*
* @returns {boolean} Return true if given string is all number, false otherwise
*
*/
function isNumber(d) {
for (let i = 0; i < d.length; i++) {
if (!numCharators[d[i]]) {
return false;
}
}
return true;
}
/**
* Test whether or not given string is all hex
*
* @param {string} d Input data
*
* @returns {boolean} Return true if given string is all hex, false otherwise
*
*/
function isHex(d) {
let dd = d.toLowerCase();
for (let i = 0; i < dd.length; i++) {
if (!hexCharators[dd[i]]) {
return false;
}
}
return true;
}
/**
* Test whether or not given string is all hex
*
* @param {string} d Input data
*
* @returns {boolean} Return true if given string is all hex, false otherwise
*
*/
function isHostname(d) {
let dd = d.toLowerCase();
for (let i = 0; i < dd.length; i++) {
if (!hostnameCharators[dd[i]]) {
return false;
}
}
return true;
}
/**
* Parse IPv4 address
*
* @param {string} d IP address
*
* @returns {Uint8Array} Parsed IPv4 Address
*
* @throws {Exception} When the given ip address was not an IPv4 addr
*
*/
export function parseIPv4(d) {
const addrSeg = 4;
let s = d.split(".");
if (s.length != addrSeg) {
throw new Exception("Invalid address");
}
let r = new Uint8Array(addrSeg);
for (let i in s) {
if (!isNumber(s[i])) {
throw new Exception("Invalid address");
}
let ii = parseInt(s[i], 10); // Only support dec
if (isNaN(ii)) {
throw new Exception("Invalid address");
}
if (ii > 0xff) {
throw new Exception("Invalid address");
}
r[i] = ii;
}
return r;
}
/**
* Parse IPv6 address. ::ffff: notation is NOT supported
*
* @param {string} d IP address
*
* @returns {Uint16Array} Parsed IPv6 Address
*
* @throws {Exception} When the given ip address was not an IPv6 addr
*
*/
export function parseIPv6(d) {
const addrSeg = 8;
let s = d.split(":");
if (s.length > addrSeg || s.length <= 1) {
throw new Exception("Invalid address");
}
if (s[0].charAt(0) === "[") {
s[0] = s[0].substring(1, s[0].length);
let end = s.length - 1;
if (s[end].charAt(s[end].length - 1) !== "]") {
throw new Exception("Invalid address");
}
s[end] = s[end].substring(0, s[end].length - 1);
}
let r = new Uint16Array(addrSeg),
rIndexShift = 0;
for (let i = 0; i < s.length; i++) {
if (s[i].length <= 0) {
rIndexShift = addrSeg - s.length;
continue;
}
if (!isHex(s[i])) {
throw new Exception("Invalid address");
}
let ii = parseInt(s[i], 16); // Only support hex
if (isNaN(ii)) {
throw new Exception("Invalid address");
}
if (ii > 0xffff) {
throw new Exception("Invalid address");
}
r[rIndexShift + i] = ii;
}
return r;
}
/**
* Convert string into a {Uint8Array}
*
* @param {string} d Input
*
* @returns {Uint8Array} Output
*
*/
export function strToUint8Array(d) {
let r = new Uint8Array(d.length);
for (let i = 0, j = d.length; i < j; i++) {
r[i] = d.charCodeAt(i);
}
return r;
}
/**
* Parse IPv6 address. ::ffff: notation is NOT supported
*
* @param {string} d IP address
*
* @returns {Uint8Array} Parsed IPv6 Address
*
* @throws {Exception} When the given ip address was not an IPv6 addr
*
*/
export function parseHostname(d) {
if (d.length <= 0) {
throw new Exception("Invalid address");
}
if (!isHostname(d)) {
throw new Exception("Invalid address");
}
return strToUint8Array(d);
}
function parseIP(d) {
try {
return {
type: "IPv4",
data: parseIPv4(d)
};
} catch (e) {
// Do nothing
}
try {
return {
type: "IPv6",
data: new Uint8Array(parseIPv6(d).buffer)
};
} catch (e) {
// Do nothing
}
return {
type: "Hostname",
data: parseHostname(d)
};
}
export function splitHostPort(d, defPort) {
let hps = d.lastIndexOf(":"),
fhps = d.indexOf(":"),
ipv6hps = d.indexOf("[");
if ((hps < 0 || hps != fhps) && ipv6hps < 0) {
let a = parseIP(d);
return {
type: a.type,
addr: a.data,
port: defPort
};
}
if (ipv6hps > 0) {
throw new Exception("Invalid address");
} else if (ipv6hps === 0) {
let ipv6hpse = d.lastIndexOf("]");
if (ipv6hpse <= ipv6hps || ipv6hpse + 1 != hps) {
throw new Exception("Invalid address");
}
}
let addr = d.slice(0, hps),
port = d.slice(hps + 1, d.length);
if (!isNumber(port)) {
throw new Exception("Invalid address");
}
let portNum = parseInt(port, 10),
a = parseIP(addr);
return {
type: a.type,
addr: a.data,
port: portNum
};
}

278
ui/commands/common_test.js Normal file
View File

@@ -0,0 +1,278 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 assert from "assert";
import * as common from "./common.js";
describe("Common", () => {
it("parseIPv4", () => {
let tests = [
{
sample: "127.0.0.1",
expectingFailure: false,
expected: new Uint8Array([127, 0, 0, 1])
},
{
sample: "255.255.255.255",
expectingFailure: false,
expected: new Uint8Array([255, 255, 255, 255])
},
{
sample: "255.255.a.255",
expectingFailure: true,
expected: null
},
{
sample: "255.255.255",
expectingFailure: true,
expected: null
},
{
sample: "2001:db8:1f70::999:de8:7648:6e8",
expectingFailure: true,
expected: null
},
{
sample: "a.ssh.vaguly.com",
expectingFailure: true,
expected: null
}
];
for (let i in tests) {
if (tests[i].expectingFailure) {
let ee = null;
try {
common.parseIPv4(tests[i].sample);
} catch (e) {
ee = e;
}
assert.notEqual(ee, null, "Test " + tests[i].sample);
} else {
let data = common.parseIPv4(tests[i].sample);
assert.deepEqual(data, tests[i].expected);
}
}
});
it("parseIPv6", () => {
let tests = [
{
sample: "2001:db8:1f70:0:999:de8:7648:6e8",
expectingFailure: false,
expected: new Uint16Array([
0x2001,
0xdb8,
0x1f70,
0x0,
0x999,
0xde8,
0x7648,
0x6e8
])
},
{
sample: "2001:db8:85a3::8a2e:370:7334",
expectingFailure: false,
expected: new Uint16Array([
0x2001,
0xdb8,
0x85a3,
0x0,
0x0,
0x8a2e,
0x370,
0x7334
])
},
{
sample: "::1",
expectingFailure: false,
expected: new Uint16Array([0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01])
},
{
sample: "::",
expectingFailure: false,
expected: new Uint16Array([0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x00])
},
{
sample: "2001:db8:1f70::999:de8:7648:6e8",
expectingFailure: false,
expected: new Uint16Array([
0x2001,
0xdb8,
0x1f70,
0x0,
0x999,
0xde8,
0x7648,
0x6e8
])
},
{
sample: "2001:0db8:ac10:fe01::",
expectingFailure: false,
expected: new Uint16Array([
0x2001,
0x0db8,
0xac10,
0xfe01,
0x0,
0x0,
0x0,
0x0
])
},
{
sample: "::7f00:1",
expectingFailure: false,
expected: new Uint16Array([
0x0000,
0x0000,
0x0000,
0x0000,
0x0000,
0x0000,
0x7f00,
0x0001
])
},
{
sample: "127.0.0.1",
expectingFailure: true,
expected: null
},
{
sample: "255.255.255.255",
expectingFailure: true,
expected: null
},
{
sample: "255.255.a.255",
expectingFailure: true,
expected: null
},
{
sample: "255.255.255",
expectingFailure: true,
expected: null
},
{
sample: "a.ssh.vaguly.com",
expectingFailure: true,
expected: null
}
];
for (let i in tests) {
if (tests[i].expectingFailure) {
let ee = null;
try {
common.parseIPv6(tests[i].sample);
} catch (e) {
ee = e;
}
assert.notEqual(ee, null, "Test " + tests[i].sample);
} else {
let data = common.parseIPv6(tests[i].sample);
assert.deepEqual(data, tests[i].expected);
}
}
});
it("splitHostPort", () => {
let tests = [
// Host name
{
sample: "ssh.vaguly.com",
expectedType: "Hostname",
expectedAddr: common.strToUint8Array("ssh.vaguly.com"),
expectedPort: 22
},
{
sample: "ssh.vaguly.com:22",
expectedType: "Hostname",
expectedAddr: common.strToUint8Array("ssh.vaguly.com"),
expectedPort: 22
},
// IPv4
{
sample: "10.220.179.110",
expectedType: "IPv4",
expectedAddr: new Uint8Array([10, 220, 179, 110]),
expectedPort: 22
},
{
sample: "10.220.179.110:3333",
expectedType: "IPv4",
expectedAddr: new Uint8Array([10, 220, 179, 110]),
expectedPort: 3333
},
// IPv6
{
sample: "2001:db8:1f70::999:de8:7648:6e8",
expectedType: "IPv6",
expectedAddr: new Uint8Array(
new Uint16Array([
0x2001,
0xdb8,
0x1f70,
0x0,
0x999,
0xde8,
0x7648,
0x6e8
]).buffer
),
expectedPort: 22
},
{
sample: "[2001:db8:1f70::999:de8:7648:6e8]:100",
expectedType: "IPv6",
expectedAddr: new Uint8Array(
new Uint16Array([
0x2001,
0xdb8,
0x1f70,
0x0,
0x999,
0xde8,
0x7648,
0x6e8
]).buffer
),
expectedPort: 100
}
];
for (let i in tests) {
let hostport = common.splitHostPort(tests[i].sample, 22);
assert.deepEqual(hostport.type, tests[i].expectedType);
assert.deepEqual(hostport.addr, tests[i].expectedAddr);
assert.equal(hostport.port, tests[i].expectedPort);
}
});
});

61
ui/commands/controls.js vendored Normal file
View File

@@ -0,0 +1,61 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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";
export class Controls {
/**
* constructor
*
* @param {[]object} controls
*
* @throws {Exception} When control type already been defined
*
*/
constructor(controls) {
this.controls = {};
for (let i in controls) {
let cType = controls[i].type();
if (typeof this.controls[cType] === "object") {
throw new Exception('Control "' + cType + '" already been defined');
}
this.controls[cType] = controls[i];
}
}
/**
* Get a control
*
* @param {string} type Type of the control
* @param {...any} data Data needed to build the control
*
* @returns {object} Control object
*
* @throws {Exception} When given control type is undefined
*
*/
get(type, ...data) {
if (typeof this.controls[type] !== "object") {
throw new Exception('Control "' + type + '" was undefined');
}
return this.controls[type].build(...data);
}
}

106
ui/commands/events.js Normal file
View File

@@ -0,0 +1,106 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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";
export class Events {
/**
* constructor
*
* @param {[]string} events required events
* @param {object} callbacks Callbacks
*
* @throws {Exception} When event handler is not registered
*
*/
constructor(events, callbacks) {
this.events = {};
this.placeHolders = {};
for (let i in events) {
if (typeof callbacks[events[i]] !== "function") {
throw new Exception(
'Unknown event type for "' +
events[i] +
'". Expecting "function" got "' +
typeof callbacks[events[i]] +
'" instead.'
);
}
let name = events[i];
if (name.indexOf("@") === 0) {
name = name.substring(1);
this.placeHolders[name] = null;
}
this.events[name] = callbacks[events[i]];
}
}
/**
* Place callbacks to pending placeholder events
*
* @param {string} type Event Type
* @param {function} callback Callback function
*/
place(type, callback) {
if (this.placeHolders[type] !== null) {
throw new Exception(
'Event type "' +
type +
'" cannot be appended. It maybe ' +
"unregistered or already been acquired"
);
}
if (typeof callback !== "function") {
throw new Exception(
'Unknown event type for "' +
type +
'". Expecting "function" got "' +
typeof callback +
'" instead.'
);
}
delete this.placeHolders[type];
this.events[type] = callback;
}
/**
* Fire an event
*
* @param {string} type Event type
* @param {...any} data Event data
*
* @returns {any} The result of the event handler
*
* @throws {Exception} When event type is not registered
*
*/
fire(type, ...data) {
if (!this.events[type] && this.placeHolders[type] !== null) {
throw new Exception("Unknown event type: " + type);
}
return this.events[type](...data);
}
}

38
ui/commands/exception.js Normal file
View File

@@ -0,0 +1,38 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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/>.
export default class Exception {
/**
* constructor
*
* @param {string} message error message
*
*/
constructor(message) {
this.message = message;
}
/**
* Return the error string
*
* @returns {string} Error message
*
*/
toString() {
return this.message;
}
}

115
ui/commands/history.js Normal file
View File

@@ -0,0 +1,115 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 * as command from "./commands.js";
export class History {
/**
* constructor
*
* @param {array<object>} records
* @param {function} saver
* @param {number} maxItems
*
*/
constructor(records, saver, maxItems) {
this.records = records;
this.maxItems = maxItems;
this.saver = saver;
}
/**
* Save record to history
*
* @param {string} uname unique name
* @param {string} title Title
* @param {command.Info} info Command info
* @param {Date} lastUsed Last used
* @param {object} data Data
*
*/
save(uname, title, lastUsed, info, data) {
for (let i in this.records) {
if (this.records[i].uname !== uname) {
continue;
}
this.records.splice(i, 1);
break;
}
this.records.push({
uname: uname,
title: title,
type: info.name(),
color: info.color(),
last: lastUsed.getTime(),
data: data
});
if (this.records.length > this.maxItems) {
this.records = this.records.slice(
this.records.length - this.maxItems,
this.records.length
);
}
this.saver(this, this.records);
}
/**
* Save record to history
*
* @param {string} uid unique name
*
*/
del(uid) {
for (let i in this.records) {
if (this.records[i].uname !== uid) {
continue;
}
this.records.splice(i, 1);
break;
}
this.saver(this, this.records);
}
/**
* Return all history records
*
* @returns {array<object>} Records
*
*/
all() {
let r = [];
for (let i in this.records) {
r.push({
uid: this.records[i].uname,
title: this.records[i].title,
type: this.records[i].type,
color: this.records[i].color,
last: new Date(this.records[i].last),
data: this.records[i].data
});
}
return r;
}
}

90
ui/commands/integer.js Normal file
View File

@@ -0,0 +1,90 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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";
import * as reader from "../stream/reader.js";
export const MAX = 0x3fff;
export const MAX_BYTES = 2;
const integerHasNextBit = 0x80;
const integerValueCutter = 0x7f;
export class Integer {
/**
* constructor
*
* @param {number} num Integer number
*
*/
constructor(num) {
this.num = num;
}
/**
* Marshal integer to buffer
*
* @returns {Uint8Array} Integer buffer
*
* @throws {Exception} When number is too large
*
*/
marshal() {
if (this.num > MAX) {
throw new Exception("Integer number cannot be greater than 0x3fff");
}
if (this.num <= integerValueCutter) {
return new Uint8Array([this.num & integerValueCutter]);
}
return new Uint8Array([
(this.num >> 7) | integerHasNextBit,
this.num & integerValueCutter
]);
}
/**
* Parse the reader to build an Integer
*
* @param {reader.Reader} rd Data reader
*
*/
async unmarshal(rd) {
for (let i = 0; i < MAX_BYTES; i++) {
let r = await reader.readOne(rd);
this.num |= r[0] & integerValueCutter;
if ((integerHasNextBit & r[0]) == 0) {
return;
}
this.num <<= 7;
}
}
/**
* Return the value of the number
*
* @returns {number} The integer value
*
*/
value() {
return this.num;
}
}

View File

@@ -0,0 +1,60 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 assert from "assert";
import * as reader from "../stream/reader.js";
import * as integer from "./integer.js";
describe("Integer", () => {
it("Integer 127", async () => {
let i = new integer.Integer(127),
marshalled = i.marshal();
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
assert.equal(marshalled.length, 1);
r.feed(marshalled);
let i2 = new integer.Integer(0);
await i2.unmarshal(r);
assert.equal(i.value(), i2.value());
});
it("Integer MAX", async () => {
let i = new integer.Integer(integer.MAX),
marshalled = i.marshal();
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
assert.equal(marshalled.length, 2);
r.feed(marshalled);
let i2 = new integer.Integer(0);
await i2.unmarshal(r);
assert.equal(i.value(), i2.value());
});
});

801
ui/commands/ssh.js Normal file
View File

@@ -0,0 +1,801 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 * 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 Exception from "./exception.js";
const AUTHMETHOD_NONE = 0x01;
const AUTHMETHOD_PASSPHARSE = 0x01;
const AUTHMETHOD_PRIVATE_KEY = 0x02;
const COMMAND_ID = 0x01;
const MAX_USERNAME_LEN = 64;
const MAX_PASSWORD_LEN = 4096;
const DEFAULT_PORT = 22;
const SERVER_REMOTE_STDOUT = 0x00;
const SERVER_REMOTE_STDERR = 0x01;
const SERVER_CONNECT_FAILED = 0x02;
const SERVER_CONNECTED = 0x03;
const SERVER_CONNECT_REQUEST_FINGERPRINT = 0x04;
const SERVER_CONNECT_REQUEST_CREDENTIAL = 0x05;
const CLIENT_DATA_STDIN = 0x00;
const CLIENT_DATA_RESIZE = 0x01;
const CLIENT_CONNECT_RESPOND_FINGERPRINT = 0x02;
const CLIENT_CONNECT_RESPOND_CREDENTIAL = 0x03;
const SERVER_REQUEST_ERROR_BAD_USERNAME = 0x01;
const SERVER_REQUEST_ERROR_BAD_ADDRESS = 0x02;
const SERVER_REQUEST_ERROR_BAD_AUTHMETHOD = 0x03;
const FingerprintPromptVerifyPassed = 0x00;
const FingerprintPromptVerifyNoRecord = 0x01;
const FingerprintPromptVerifyMismatch = 0x02;
class SSH {
/**
* constructor
*
* @param {stream.Sender} sd Stream sender
* @param {object} config configuration
* @param {object} callbacks Event callbacks
*
*/
constructor(sd, config, callbacks) {
this.sender = sd;
this.config = config;
this.connected = false;
this.events = new event.Events(
[
"initialization.failed",
"initialized",
"connect.failed",
"connect.succeed",
"connect.fingerprint",
"connect.credential",
"@stdout",
"@stderr",
"close",
"@completed"
],
callbacks
);
}
/**
* Send intial request
*
* @param {stream.InitialSender} initialSender Initial stream request sender
*
*/
run(initialSender) {
let user = new strings.String(this.config.user),
userBuf = user.buffer(),
addr = new address.Address(
this.config.host.type,
this.config.host.address,
this.config.host.port
),
addrBuf = addr.buffer(),
authMethod = new Uint8Array([this.config.auth]);
let data = new Uint8Array(userBuf.length + addrBuf.length + 1);
data.set(userBuf, 0);
data.set(addrBuf, userBuf.length);
data.set(authMethod, userBuf.length + addrBuf.length);
initialSender.send(data);
}
/**
* Receive the initial stream request
*
* @param {header.InitialStream} streamInitialHeader Server respond on the
* initial stream request
*
*/
initialize(streamInitialHeader) {
if (!streamInitialHeader.success()) {
this.events.fire("initialization.failed", streamInitialHeader);
return;
}
this.events.fire("initialized", streamInitialHeader);
}
/**
* Tick the command
*
* @param {header.Stream} streamHeader Stream data header
* @param {reader.Limited} rd Data reader
*
* @returns {any} The result of the ticking
*
* @throws {Exception} When the stream header type is unknown
*
*/
tick(streamHeader, rd) {
switch (streamHeader.marker()) {
case SERVER_CONNECTED:
if (!this.connected) {
this.connected = true;
return this.events.fire("connect.succeed", rd, this);
}
break;
case SERVER_CONNECT_FAILED:
if (!this.connected) {
return this.events.fire("connect.failed", rd);
}
break;
case SERVER_CONNECT_REQUEST_FINGERPRINT:
if (!this.connected) {
return this.events.fire("connect.fingerprint", rd, this.sender);
}
break;
case SERVER_CONNECT_REQUEST_CREDENTIAL:
if (!this.connected) {
return this.events.fire("connect.credential", rd, this.sender);
}
break;
case SERVER_REMOTE_STDOUT:
if (this.connected) {
return this.events.fire("stdout", rd);
}
break;
case SERVER_REMOTE_STDERR:
if (this.connected) {
return this.events.fire("stderr", rd);
}
break;
}
throw new Exception("Unknown stream header marker");
}
/**
* Send close signal to remote
*
*/
async sendClose() {
return await this.sender.close();
}
/**
* Send data to remote
*
* @param {Uint8Array} data
*
*/
async sendData(data) {
return this.sender.send(CLIENT_DATA_STDIN, data);
}
/**
* Send resize request
*
* @param {number} rows
* @param {number} cols
*
*/
async sendResize(rows, cols) {
let data = new DataView(new ArrayBuffer(4));
data.setUint16(0, rows);
data.setUint16(2, cols);
return this.sender.send(CLIENT_DATA_RESIZE, new Uint8Array(data.buffer));
}
/**
* Close the command
*
*/
async close() {
await this.sendClose();
return this.events.fire("close");
}
/**
* Tear down the command completely
*
*/
completed() {
return this.events.fire("completed");
}
}
const initialFieldDef = {
User: {
name: "User",
description: "",
type: "text",
value: "",
example: "root",
verify(d) {
if (d.length <= 0) {
throw new Error("Username must be specified");
}
if (d.length > MAX_USERNAME_LEN) {
throw new Error(
"Username must not longer than " + MAX_USERNAME_LEN + " bytes"
);
}
return "We'll login as user \"" + d + '"';
}
},
Host: {
name: "Host",
description: "",
type: "text",
value: "",
example: "ssh.vaguly.com:22",
verify(d) {
if (d.length <= 0) {
throw new Error("Hostname must be specified");
}
let addr = common.splitHostPort(d, DEFAULT_PORT);
if (addr.addr.length <= 0) {
throw new Error("Cannot be empty");
}
if (addr.addr.length > address.MAX_ADDR_LEN) {
throw new Error(
"Can no longer than " + address.MAX_ADDR_LEN + " bytes"
);
}
if (addr.port <= 0) {
throw new Error("Port must be specified");
}
return "Look like " + addr.type + " address";
}
},
Notice: {
name: "Notice",
description: "",
type: "textdata",
value:
"SSH session is handled by the backend. Traffic will be decrypted " +
"on the backend server and then be transmitted back to your client.",
example: "",
verify(d) {
return "";
}
},
Passpharse: {
name: "Passpharse",
description: "",
type: "password",
value: "",
example: "----------",
verify(d) {
if (d.length <= 0) {
throw new Error("Passpharse must be specified");
}
if (d.length > MAX_PASSWORD_LEN) {
throw new Error(
"It's too long, make it shorter than " + MAX_PASSWORD_LEN + " bytes"
);
}
return "We'll login with this passpharse";
}
},
"Private Key": {
name: "Private Key",
description: "Like the one inside ~/.ssh/id_rsa, can&apos;t be encrypted",
type: "textarea",
value: "",
example:
"-----BEGIN RSA PRIVATE KEY-----\r\n" +
"..... yBQZobkBQ50QqhDivQz4i1Pb33Z0Znjnzjoid4 ....\r\n" +
"-----END RSA PRIVATE KEY-----",
verify(d) {
if (d.length <= 0) {
throw new Error("Private Key must be specified");
}
if (d.length > MAX_PASSWORD_LEN) {
throw new Error(
"It's too long, make it shorter than " + MAX_PASSWORD_LEN + " bytes"
);
}
return "We'll login with this private key";
}
},
Authentication: {
name: "Authentication",
description:
"Please make sure the authentication method that you selected is " +
"supported by the server, otherwise it will be ignored and likely " +
"cause the login to fail",
type: "radio",
value: "",
example: "Password,Private Key,None",
verify(d) {
switch (d) {
case "Password":
case "Private Key":
case "None":
return "";
default:
throw new Error("Authentication method must be specified");
}
}
},
Fingerprint: {
name: "Fingerprint",
description:
"Please carefully verify the fingerprint. DO NOT continue " +
"if the fingerprint is unknown to you, otherwise you maybe " +
"giving your own secrets to an imposter",
type: "textdata",
value: "",
example: "",
verify(d) {
return "";
}
}
};
/**
* Return auth method from given string
*
* @param {string} d string data
*
* @returns {number} Auth method
*
* @throws {Exception} When auth method is invalid
*
*/
function getAuthMethodFromStr(d) {
switch (d) {
case "None":
return AUTHMETHOD_NONE;
case "Password":
return AUTHMETHOD_PASSPHARSE;
case "Private Key":
return AUTHMETHOD_PRIVATE_KEY;
default:
throw new Exception("Unknown Auth method");
}
}
class Wizard {
/**
* constructor
*
* @param {command.Info} info
* @param {object} config
* @param {streams.Streams} streams
* @param {subscribe.Subscribe} subs
* @param {controls.Controls} controls
* @param {history.History} history
*
*/
constructor(info, config, streams, subs, controls, history) {
this.info = info;
this.hasStarted = false;
this.streams = streams;
this.config = config;
this.step = subs;
this.controls = controls;
this.history = history;
this.step.resolve(this.stepInitialPrompt());
}
started() {
return this.hasStarted;
}
close() {
this.step.resolve(
this.stepErrorDone(
"Action cancelled",
"Action has been cancelled without reach any success"
)
);
}
stepErrorDone(title, message) {
return command.done(false, null, title, message);
}
stepSuccessfulDone(data) {
return command.done(
true,
data,
"Success!",
"We have connected to the remote"
);
}
stepWaitForAcceptWait() {
return command.wait(
"Requesting",
"Waiting for request to be accepted by the backend"
);
}
stepWaitForEstablishWait(host) {
return command.wait(
"Connecting to " + host,
"Establishing connection with the remote host, may take a while"
);
}
stepContinueWaitForEstablishWait() {
return command.wait(
"Connecting",
"Establishing connection with the remote host, may take a while"
);
}
/**
*
* @param {stream.Sender} sender
* @param {object} configInput
*
*/
buildCommand(sender, configInput) {
let self = this;
let config = {
user: common.strToUint8Array(configInput.user),
auth: getAuthMethodFromStr(configInput.authentication),
host: address.parseHostPort(configInput.host, DEFAULT_PORT),
fingerprint: configInput.fingerprint
};
return new SSH(sender, config, {
"initialization.failed"(hd) {
switch (hd.data()) {
case SERVER_REQUEST_ERROR_BAD_USERNAME:
self.step.resolve(
self.stepErrorDone("Request failed", "Invalid username")
);
return;
case SERVER_REQUEST_ERROR_BAD_ADDRESS:
self.step.resolve(
self.stepErrorDone("Request failed", "Invalid address")
);
return;
case SERVER_REQUEST_ERROR_BAD_AUTHMETHOD:
self.step.resolve(
self.stepErrorDone("Request failed", "Invalid authication method")
);
return;
}
self.step.resolve(
self.stepErrorDone("Request failed", "Unknown error: " + hd.data())
);
},
initialized(hd) {
self.step.resolve(self.stepWaitForEstablishWait(configInput.host));
},
async "connect.failed"(rd) {
let d = new TextDecoder("utf-8").decode(
await reader.readCompletely(rd)
);
self.step.resolve(self.stepErrorDone("Connection failed", d));
},
"connect.succeed"(rd, commandHandler) {
self.connectionSucceed = true;
self.step.resolve(
self.stepSuccessfulDone(
new command.Result(
configInput.user + "@" + configInput.host,
self.info,
self.controls.get("SSH", {
send(data) {
return commandHandler.sendData(data);
},
close() {
return commandHandler.sendClose();
},
resize(rows, cols) {
return commandHandler.sendResize(rows, cols);
},
events: commandHandler.events
})
)
)
);
self.history.save(
self.info.name() + ":" + configInput.user + "@" + configInput.host,
configInput.user + "@" + configInput.host,
new Date(),
self.info,
configInput
);
},
async "connect.fingerprint"(rd, sd) {
self.step.resolve(
await self.stepFingerprintPrompt(
rd,
sd,
v => {
if (!configInput.fingerprint) {
return FingerprintPromptVerifyNoRecord;
}
if (configInput.fingerprint === v) {
return FingerprintPromptVerifyPassed;
}
return FingerprintPromptVerifyMismatch;
},
newFingerprint => {
configInput.fingerprint = newFingerprint;
}
)
);
},
async "connect.credential"(rd, sd) {
self.step.resolve(self.stepCredentialPrompt(rd, sd, config));
},
"@stdout"(rd) {},
"@stderr"(rd) {},
close() {},
"@completed"() {
self.step.resolve(
self.stepErrorDone(
"Operation has failed",
"Connection has been cancelled"
)
);
}
});
}
stepInitialPrompt() {
let self = this;
if (this.config) {
self.hasStarted = true;
self.streams.request(COMMAND_ID, sd => {
return self.buildCommand(sd, this.config);
});
return self.stepWaitForAcceptWait();
}
return command.prompt(
"SSH",
"Secure Shell Host",
"Connect",
r => {
self.hasStarted = true;
self.streams.request(COMMAND_ID, sd => {
return self.buildCommand(sd, {
user: r.user,
authentication: r.authentication,
host: r.host,
fingerprint: ""
});
});
self.step.resolve(self.stepWaitForAcceptWait());
},
() => {},
command.fields(initialFieldDef, [
{ name: "User" },
{ name: "Host" },
{ name: "Authentication" },
{ name: "Notice" }
])
);
}
async stepFingerprintPrompt(rd, sd, verify, newFingerprint) {
let self = this,
fingerprintData = new TextDecoder("utf-8").decode(
await reader.readCompletely(rd)
),
fingerprintChanged = false;
switch (verify(fingerprintData)) {
case FingerprintPromptVerifyPassed:
sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([0]));
return this.stepContinueWaitForEstablishWait();
case FingerprintPromptVerifyMismatch:
fingerprintChanged = true;
}
return command.prompt(
!fingerprintChanged
? "Do you recognize this server?"
: "Danger! Server fingerprint has changed!",
!fingerprintChanged
? "Verify server fingerprint displayed below"
: "It's very unusual. Please verify the new server fingerprint below",
!fingerprintChanged ? "Yes, I do" : "I'm aware of the change",
r => {
newFingerprint(fingerprintData);
sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([0]));
self.step.resolve(self.stepContinueWaitForEstablishWait());
},
() => {
sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([1]));
self.step.resolve(
command.wait("Rejecting", "Sending rejection to the backend")
);
},
command.fields(initialFieldDef, [
{
name: "Fingerprint",
value: fingerprintData
}
])
);
}
async stepCredentialPrompt(rd, sd, config) {
let self = this,
fieldName = "";
switch (config.auth) {
case AUTHMETHOD_PASSPHARSE:
fieldName = "Passpharse";
break;
case AUTHMETHOD_PRIVATE_KEY:
fieldName = "Private Key";
break;
default:
throw new Exception(
"Prompt is not support by auth method: " + config.auth
);
}
return command.prompt(
"Provide credential",
"Please input your credential",
"Login",
r => {
let vv = r[fieldName.toLowerCase()];
sd.send(
CLIENT_CONNECT_RESPOND_CREDENTIAL,
new TextEncoder("utf-8").encode(vv)
);
self.step.resolve(self.stepContinueWaitForEstablishWait());
},
() => {
sd.close();
self.step.resolve(
command.wait(
"Cancelling login",
"Cancelling login request, please wait"
)
);
},
command.fields(initialFieldDef, [{ name: fieldName }])
);
}
}
export class Command {
constructor() {}
id() {
return COMMAND_ID;
}
name() {
return "SSH";
}
description() {
return "Secure Shell Host";
}
color() {
return "#3c8";
}
builder(info, config, streams, subs, controls, history) {
return new Wizard(info, config, streams, subs, controls, history);
}
launch(info, launcher, streams, subs, controls, history) {
let matchResult = launcher.match(new RegExp("^(.*)\\@(.*)\\|(.*)$"));
if (!matchResult || matchResult.length !== 4) {
throw new Exception('Given launcher "' + launcher + '" was malformed');
}
let user = matchResult[1],
host = matchResult[2],
auth = matchResult[3];
try {
initialFieldDef["User"].verify(user);
initialFieldDef["Host"].verify(host);
initialFieldDef["Authentication"].verify(auth);
} catch (e) {
throw new Exception(
'Given launcher "' + launcher + '" was malformed ' + e
);
}
return this.builder(
info,
{
user: user,
host: host,
authentication: auth
},
streams,
subs,
controls,
history
);
}
launcher(config) {
return config.user + "@" + config.host + "|" + config.authentication;
}
}

72
ui/commands/string.js Normal file
View File

@@ -0,0 +1,72 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 * as reader from "../stream/reader.js";
import * as integer from "./integer.js";
export class String {
/**
* Read String from given reader
*
* @param {reader.Reader} rd Source reader
*
* @returns {String} readed string
*
*/
static async read(rd) {
let l = new integer.Integer(0);
await l.unmarshal(rd);
return new String(await reader.readN(rd, l.value()));
}
/**
* constructor
*
* @param {Uint8Array} str String data
*/
constructor(str) {
this.str = str;
}
/**
* Return the string
*
* @returns {Uint8Array} String data
*
*/
data() {
return this.str;
}
/**
* Return serialized String as array
*
* @returns {Uint8Array} serialized String
*
*/
buffer() {
let lBytes = new integer.Integer(this.str.length).marshal(),
buf = new Uint8Array(lBytes.length + this.str.length);
buf.set(lBytes, 0);
buf.set(this.str, lBytes.length);
return buf;
}
}

265
ui/commands/string_test.js Normal file
View File

@@ -0,0 +1,265 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 * as strings from "./string.js";
import * as reader from "../stream/reader.js";
import assert from "assert";
describe("String", () => {
it("String 1", async () => {
let s = new strings.String(new Uint8Array(["H", "E", "L", "L", "O"])),
sBuf = s.buffer();
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
r.feed(sBuf);
let s2 = await strings.String.read(r);
assert.deepEqual(s2.data(), s.data());
});
it("String 2", async () => {
let s = new strings.String(
new Uint8Array([
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D",
"H",
"E",
"L",
"L",
"O",
"W",
"O",
"R",
"L",
"D"
])
),
sBuf = s.buffer();
let r = new reader.Reader(new reader.Multiple(() => {}), data => {
return data;
});
r.feed(sBuf);
let s2 = await strings.String.read(r);
assert.deepEqual(s2.data(), s.data());
});
});

430
ui/commands/telnet.js Normal file
View File

@@ -0,0 +1,430 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019 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 * 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 Exception from "./exception.js";
const COMMAND_ID = 0x00;
const SERVER_INITIAL_ERROR_BAD_ADDRESS = 0x01;
const SERVER_REMOTE_BAND = 0x00;
const SERVER_DIAL_FAILED = 0x01;
const SERVER_DIAL_CONNECTED = 0x02;
const DEFAULT_PORT = 23;
class Telnet {
/**
* constructor
*
* @param {stream.Sender} sd Stream sender
* @param {object} config configuration
* @param {object} callbacks Event callbacks
*
*/
constructor(sd, config, callbacks) {
this.sender = sd;
this.config = config;
this.connected = false;
this.events = new event.Events(
[
"initialization.failed",
"initialized",
"connect.failed",
"connect.succeed",
"@inband",
"close",
"@completed"
],
callbacks
);
}
/**
* Send intial request
*
* @param {stream.InitialSender} initialSender Initial stream request sender
*
*/
run(initialSender) {
let addr = new address.Address(
this.config.host.type,
this.config.host.address,
this.config.host.port
),
addrBuf = addr.buffer();
let data = new Uint8Array(addrBuf.length);
data.set(addrBuf, 0);
initialSender.send(data);
}
/**
* Receive the initial stream request
*
* @param {header.InitialStream} streamInitialHeader Server respond on the
* initial stream request
*
*/
initialize(streamInitialHeader) {
if (!streamInitialHeader.success()) {
this.events.fire("initialization.failed", streamInitialHeader);
return;
}
this.events.fire("initialized", streamInitialHeader);
}
/**
* Tick the command
*
* @param {header.Stream} streamHeader Stream data header
* @param {reader.Limited} rd Data reader
*
* @returns {any} The result of the ticking
*
* @throws {Exception} When the stream header type is unknown
*
*/
tick(streamHeader, rd) {
switch (streamHeader.marker()) {
case SERVER_DIAL_CONNECTED:
if (!this.connected) {
this.connected = true;
return this.events.fire("connect.succeed", rd, this);
}
break;
case SERVER_DIAL_FAILED:
if (!this.connected) {
return this.events.fire("connect.failed", rd);
}
break;
case SERVER_REMOTE_BAND:
if (this.connected) {
return this.events.fire("inband", rd);
}
break;
}
throw new Exception("Unknown stream header marker");
}
/**
* Send close signal to remote
*
*/
sendClose() {
return this.sender.close();
}
/**
* Send data to remote
*
* @param {Uint8Array} data
*
*/
sendData(data) {
return this.sender.send(0x00, data);
}
/**
* Close the command
*
*/
close() {
this.sendClose();
return this.events.fire("close");
}
/**
* Tear down the command completely
*
*/
completed() {
return this.events.fire("completed");
}
}
const initialFieldDef = {
Host: {
name: "Host",
description:
"Looking for server to connect&quest; Checkout " +
'<a href="http://www.telnet.org/htm/places.htm" target="blank">' +
"telnet.org</a> for public servers.",
type: "text",
value: "",
example: "telnet.vaguly.com:23",
verify(d) {
if (d.length <= 0) {
throw new Error("Hostname must be specified");
}
let addr = common.splitHostPort(d, DEFAULT_PORT);
if (addr.addr.length <= 0) {
throw new Error("Cannot be empty");
}
if (addr.addr.length > address.MAX_ADDR_LEN) {
throw new Error(
"Can no longer than " + address.MAX_ADDR_LEN + " bytes"
);
}
if (addr.port <= 0) {
throw new Error("Port must be specified");
}
return "Look like " + addr.type + " address";
}
}
};
class Wizard {
/**
* constructor
*
* @param {command.Info} info
* @param {object} config
* @param {streams.Streams} streams
* @param {subscribe.Subscribe} subs
* @param {controls.Controls} controls
* @param {history.History} history
*
*/
constructor(info, config, streams, subs, controls, history) {
this.info = info;
this.hasStarted = false;
this.streams = streams;
this.config = config;
this.step = subs;
this.controls = controls;
this.history = history;
this.step.resolve(this.stepInitialPrompt());
}
started() {
return this.hasStarted;
}
close() {
this.step.resolve(
this.stepErrorDone(
"Action cancelled",
"Action has been cancelled without reach any success"
)
);
}
stepErrorDone(title, message) {
return command.done(false, null, title, message);
}
stepSuccessfulDone(data) {
return command.done(
true,
data,
"Success!",
"We have connected to the remote"
);
}
stepWaitForAcceptWait() {
return command.wait(
"Requesting",
"Waiting for request to be accepted by the backend"
);
}
stepWaitForEstablishWait(host) {
return command.wait(
"Connecting to " + host,
"Establishing connection with the remote host, may take a while"
);
}
/**
*
* @param {stream.Sender} sender
* @param {object} configInput
*
*/
buildCommand(sender, configInput) {
let self = this;
let parsedConfig = {
host: address.parseHostPort(configInput.host, DEFAULT_PORT)
};
return new Telnet(sender, parsedConfig, {
"initialization.failed"(streamInitialHeader) {
switch (streamInitialHeader.data()) {
case SERVER_INITIAL_ERROR_BAD_ADDRESS:
self.step.resolve(
self.stepErrorDone("Request rejected", "Invalid address")
);
return;
}
self.step.resolve(
self.stepErrorDone(
"Request rejected",
"Unknown error code: " + streamInitialHeader.data()
)
);
},
initialized(streamInitialHeader) {
self.step.resolve(self.stepWaitForEstablishWait(configInput.host));
},
"connect.succeed"(rd, commandHandler) {
self.step.resolve(
self.stepSuccessfulDone(
new command.Result(
configInput.host,
self.info,
self.controls.get("Telnet", {
send(data) {
return commandHandler.sendData(data);
},
close() {
return commandHandler.sendClose();
},
events: commandHandler.events
})
)
)
);
self.history.save(
self.info.name() + ":" + configInput.host,
configInput.host,
new Date(),
self.info,
configInput
);
},
async "connect.failed"(rd) {
let readed = await reader.readCompletely(rd),
message = new TextDecoder("utf-8").decode(readed.buffer);
self.step.resolve(self.stepErrorDone("Connection failed", message));
},
"@inband"(rd) {},
close() {},
"@completed"() {}
});
}
stepInitialPrompt() {
let self = this;
if (this.config) {
self.hasStarted = true;
self.streams.request(COMMAND_ID, sd => {
return self.buildCommand(sd, this.config);
});
return self.stepWaitForAcceptWait();
}
return command.prompt(
"Telnet",
"Teletype Network",
"Connect",
r => {
self.hasStarted = true;
self.streams.request(COMMAND_ID, sd => {
return self.buildCommand(sd, r);
});
self.step.resolve(self.stepWaitForAcceptWait());
},
() => {},
command.fields(initialFieldDef, [{ name: "Host" }])
);
}
}
export class Command {
constructor() {}
id() {
return COMMAND_ID;
}
name() {
return "Telnet";
}
description() {
return "Teletype Network";
}
color() {
return "#6ac";
}
builder(info, config, streams, subs, controls, history) {
return new Wizard(info, config, streams, subs, controls, history);
}
launch(info, launcher, streams, subs, controls, history) {
try {
initialFieldDef["Host"].verify(launcher);
} catch (e) {
throw new Exception(
'Given launcher "' + launcher + '" was invalid: ' + e
);
}
return this.builder(
info,
{
host: launcher
},
streams,
subs,
controls,
history
);
}
launcher(config) {
return config.host;
}
}