Initial commit
This commit is contained in:
228
ui/commands/address.js
Normal file
228
ui/commands/address.js
Normal 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
102
ui/commands/address_test.js
Normal 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
107
ui/commands/color.js
Normal 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
720
ui/commands/commands.js
Normal 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
360
ui/commands/common.js
Normal 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
278
ui/commands/common_test.js
Normal 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
61
ui/commands/controls.js
vendored
Normal 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
106
ui/commands/events.js
Normal 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
38
ui/commands/exception.js
Normal 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
115
ui/commands/history.js
Normal 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
90
ui/commands/integer.js
Normal 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;
|
||||
}
|
||||
}
|
||||
60
ui/commands/integer_test.js
Normal file
60
ui/commands/integer_test.js
Normal 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
801
ui/commands/ssh.js
Normal 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'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
72
ui/commands/string.js
Normal 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
265
ui/commands/string_test.js
Normal 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
430
ui/commands/telnet.js
Normal 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? 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user