clean up
This commit is contained in:
parent
b9a8c13426
commit
32ff8686f4
@ -1,230 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as common from "./common.js";
|
||||
import Exception from "./exception.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,
|
||||
};
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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.strictEqual(addr2.type(), addr.type());
|
||||
assert.deepStrictEqual(addr2.address(), addr.address());
|
||||
assert.strictEqual(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.strictEqual(addr2.type(), addr.type());
|
||||
assert.deepStrictEqual(addr2.address(), addr.address());
|
||||
assert.strictEqual(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.strictEqual(addr2.type(), addr.type());
|
||||
assert.deepStrictEqual(addr2.address(), addr.address());
|
||||
assert.strictEqual(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.strictEqual(addr2.type(), addr.type());
|
||||
assert.deepStrictEqual(addr2.address(), addr.address());
|
||||
assert.strictEqual(addr2.port(), addr.port());
|
||||
});
|
||||
});
|
||||
@ -1,107 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
@ -1,880 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as subscribe from "../stream/subscribe.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as presets from "./presets.js";
|
||||
|
||||
export const NEXT_PROMPT = 1;
|
||||
export const NEXT_WAIT = 2;
|
||||
export const NEXT_DONE = 3;
|
||||
|
||||
export class Result {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {string} name Result type
|
||||
* @param {Info} info Result info
|
||||
* @param {object} control Result controller
|
||||
* @param {string} ui User interfact this command will use
|
||||
*/
|
||||
constructor(name, info, control, ui) {
|
||||
this.name = name;
|
||||
this.info = info;
|
||||
this.control = control;
|
||||
this.ui = ui;
|
||||
}
|
||||
}
|
||||
|
||||
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: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(v) {
|
||||
return "";
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 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]) {
|
||||
n[i] = f[i];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'Field data type for "' +
|
||||
i +
|
||||
'" was unmatched. Expecting "' +
|
||||
typeof n[i] +
|
||||
'", got "' +
|
||||
typeof f[i] +
|
||||
'" instead',
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command fields with preset data
|
||||
*
|
||||
* @param {object} definitions Definition of a group of fields
|
||||
* @param {object} fieldsData field data object, formated like a `defField`
|
||||
* @param {presets.Preset} presetData Preset data
|
||||
* @param {function} presetApplied Called when a preset is used for a field
|
||||
*
|
||||
* @returns {object}
|
||||
*
|
||||
*/
|
||||
export function fieldsWithPreset(
|
||||
definitions,
|
||||
fieldsData,
|
||||
presetData,
|
||||
presetApplied,
|
||||
) {
|
||||
let newFields = fields(definitions, fieldsData);
|
||||
|
||||
for (let i in newFields) {
|
||||
try {
|
||||
newFields[i].value = presetData.meta(newFields[i].name);
|
||||
newFields[i].readonly = true;
|
||||
|
||||
presetApplied(newFields[i].name);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return newFields;
|
||||
}
|
||||
|
||||
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 {object} built Command executer
|
||||
* @param {subscribe.Subscribe} subs Wizard step subscriber
|
||||
* @param {function} done Callback which will be called when the wizard
|
||||
* is done
|
||||
*
|
||||
*/
|
||||
constructor(built, subs, done) {
|
||||
this.built = built;
|
||||
this.subs = subs;
|
||||
this.done = done;
|
||||
this.closed = false;
|
||||
|
||||
this.built.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
this.done(n);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the control info of current wizard
|
||||
*
|
||||
* @returns {object}
|
||||
*
|
||||
*/
|
||||
control() {
|
||||
return this.built.control();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.represeter = (n) => {
|
||||
return command.represet(n);
|
||||
};
|
||||
this.wizarder = (n, i, r, u, y, x, l, p) => {
|
||||
return command.wizard(n, i, r, u, y, x, l, p);
|
||||
};
|
||||
this.executer = (n, i, r, u, y, x, l, p) => {
|
||||
return command.execute(n, i, r, u, y, x, l, p);
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an automatic command wizard
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {Array<string>} keptSessions
|
||||
* @param {function} done Callback which will be called when wizard is done
|
||||
*
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
wizard(streams, controls, history, preset, session, keptSessions, done) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.wizarder(
|
||||
new Info(this),
|
||||
preset,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
),
|
||||
subs,
|
||||
done,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an automatic command wizard
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
* @param {object} config
|
||||
* @param {object} session
|
||||
* @param {Array<string>} keptSessions
|
||||
* @param {function} done Callback which will be called when wizard is done
|
||||
*
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
execute(streams, controls, history, config, session, keptSessions, done) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.executer(
|
||||
new Info(this),
|
||||
config,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
),
|
||||
subs,
|
||||
done,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {function} done Callback which will be called when launching is done
|
||||
*
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
launch(streams, controls, history, launcher, done) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.launchCmd(
|
||||
new Info(this),
|
||||
decodeURI(launcher),
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
),
|
||||
subs,
|
||||
done,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build launcher string out of given config
|
||||
*
|
||||
* @param {object} config Configuration object
|
||||
*
|
||||
* @return {string} Launcher string
|
||||
*/
|
||||
launcher(config) {
|
||||
return this.name() + ":" + encodeURI(this.launcherCmd(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconfigure the preset data for the command wizard
|
||||
*
|
||||
* @param {presets.Preset} n preset
|
||||
*
|
||||
* @return {presets.Preset} modified new preset
|
||||
*/
|
||||
represet(n) {
|
||||
return this.represeter(n);
|
||||
}
|
||||
}
|
||||
|
||||
export class Preset {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {presets.Preset} preset preset
|
||||
* @param {Builder} command executor
|
||||
*
|
||||
*/
|
||||
constructor(preset, command) {
|
||||
this.preset = preset;
|
||||
this.command = command;
|
||||
}
|
||||
}
|
||||
|
||||
export class Commands {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Array<object>} commands Command array
|
||||
*
|
||||
*/
|
||||
constructor(commands) {
|
||||
this.commands = [];
|
||||
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
this.commands.push(new Builder(commands[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all commands
|
||||
*
|
||||
* @returns {Array<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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns presets with merged command
|
||||
*
|
||||
* @param {presets.Presets} ps
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
mergePresets(ps) {
|
||||
let pp = [];
|
||||
|
||||
for (let i = 0; i < this.commands.length; i++) {
|
||||
const fetched = ps.fetch(this.commands[i].name());
|
||||
|
||||
for (let j = 0; j < fetched.length; j++) {
|
||||
pp.push(
|
||||
new Preset(this.commands[i].represet(fetched[j]), this.commands[i]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return pp;
|
||||
}
|
||||
}
|
||||
@ -1,409 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as buffer from "buffer/";
|
||||
import * as iconv from "iconv-lite";
|
||||
import Exception from "./exception.js";
|
||||
|
||||
const availableEncodings = [
|
||||
"utf-8",
|
||||
"ibm866",
|
||||
"iso-8859-2",
|
||||
"iso-8859-3",
|
||||
"iso-8859-4",
|
||||
"iso-8859-5",
|
||||
"iso-8859-6",
|
||||
"iso-8859-7",
|
||||
"iso-8859-8",
|
||||
"iso-8859-10",
|
||||
"iso-8859-13",
|
||||
"iso-8859-14",
|
||||
"iso-8859-15",
|
||||
"iso-8859-16",
|
||||
"koi8-r",
|
||||
"koi8-u",
|
||||
"macintosh",
|
||||
"windows-874",
|
||||
"windows-1250",
|
||||
"windows-1251",
|
||||
"windows-1252",
|
||||
"windows-1253",
|
||||
"windows-1254",
|
||||
"windows-1255",
|
||||
"windows-1256",
|
||||
"windows-1257",
|
||||
"windows-1258",
|
||||
"gbk",
|
||||
"gb18030",
|
||||
"big5",
|
||||
"euc-jp",
|
||||
"shift-jis",
|
||||
"euc-kr",
|
||||
"utf-16be",
|
||||
"utf-16le",
|
||||
];
|
||||
|
||||
export const charsetPresets = (() => {
|
||||
let r = [];
|
||||
|
||||
for (let i in availableEncodings) {
|
||||
try {
|
||||
if (!iconv.encodingExists(availableEncodings[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
new TextDecoder(availableEncodings[i]);
|
||||
|
||||
r.push(availableEncodings[i]);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
})();
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
export 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
|
||||
*
|
||||
*/
|
||||
export 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 a valid hostname as far as the Sshwifty
|
||||
* client consider. This function will return true if the string contains only
|
||||
* printable charactors
|
||||
*
|
||||
* @param {string} d Input data
|
||||
*
|
||||
* @returns {boolean} Return true if given string is all hex, false otherwise
|
||||
*
|
||||
*/
|
||||
function isHostname(d) {
|
||||
for (let i = 0; i < d.length; i++) {
|
||||
const dChar = d.charCodeAt(i);
|
||||
|
||||
if (dChar >= 32 && dChar <= 126) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dChar === 128) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dChar >= 130 && dChar <= 140) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dChar === 142) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dChar >= 145 && dChar <= 156) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dChar >= 158 && dChar <= 159) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dChar >= 161 && dChar <= 255) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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 {Uint8Array} 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 Uint8Array(addrSeg * 2),
|
||||
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");
|
||||
}
|
||||
let j = (rIndexShift + i) * 2;
|
||||
r[j] = ii >> 8;
|
||||
r[j + 1] = ii & 0xff;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string into a binary {Uint8Array}
|
||||
*
|
||||
* @param {string} d Input
|
||||
*
|
||||
* @returns {Uint8Array} Output
|
||||
*
|
||||
*/
|
||||
export function strToBinary(d) {
|
||||
return new Uint8Array(buffer.Buffer.from(d, "binary").buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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.notStrictEqual(ee, null, "Test " + tests[i].sample);
|
||||
} else {
|
||||
let data = common.parseIPv4(tests[i].sample);
|
||||
|
||||
assert.deepStrictEqual(data, tests[i].expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("parseIPv6", () => {
|
||||
let tests = [
|
||||
{
|
||||
sample: "2001:db8:1f70:0:999:de8:7648:6e8",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8,
|
||||
0x76, 0x48, 0x6, 0xe8,
|
||||
]),
|
||||
},
|
||||
{
|
||||
sample: "2001:db8:85a3::8a2e:370:7334",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0x20, 0x01, 0xd, 0xb8, 0x85, 0xa3, 0x0, 0x0, 0x0, 0x0, 0x8a, 0x2e,
|
||||
0x3, 0x70, 0x73, 0x34,
|
||||
]),
|
||||
},
|
||||
{
|
||||
sample: "fdef:90fb:4138::8ca",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0xfd, 0xef, 0x90, 0xfb, 0x41, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x8, 0xca,
|
||||
]),
|
||||
},
|
||||
{
|
||||
sample: "::1",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x1,
|
||||
]),
|
||||
},
|
||||
{
|
||||
sample: "::",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0,
|
||||
]),
|
||||
},
|
||||
{
|
||||
sample: "2001:db8:1f70::999:de8:7648:6e8",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8,
|
||||
0x76, 0x48, 0x6, 0xe8,
|
||||
]),
|
||||
},
|
||||
{
|
||||
sample: "2001:0db8:ac10:fe01::",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0x20, 0x01, 0x0d, 0xb8, 0xac, 0x10, 0xfe, 0x01, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0,
|
||||
]),
|
||||
},
|
||||
{
|
||||
sample: "::7f00:1",
|
||||
expectingFailure: false,
|
||||
expected: new Uint8Array([
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f,
|
||||
0x00, 0x00, 0x01,
|
||||
]),
|
||||
},
|
||||
{
|
||||
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.notStrictEqual(ee, null, "Test " + tests[i].sample);
|
||||
} else {
|
||||
let data = common.parseIPv6(tests[i].sample);
|
||||
|
||||
assert.deepStrictEqual(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([
|
||||
0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8,
|
||||
0x76, 0x48, 0x6, 0xe8,
|
||||
]),
|
||||
expectedPort: 22,
|
||||
},
|
||||
{
|
||||
sample: "[2001:db8:1f70::999:de8:7648:6e8]:100",
|
||||
expectedType: "IPv6",
|
||||
expectedAddr: new Uint8Array([
|
||||
0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8,
|
||||
0x76, 0x48, 0x6, 0xe8,
|
||||
]),
|
||||
expectedPort: 100,
|
||||
},
|
||||
];
|
||||
|
||||
for (let i in tests) {
|
||||
let hostport = common.splitHostPort(tests[i].sample, 22);
|
||||
|
||||
assert.deepStrictEqual(hostport.type, tests[i].expectedType);
|
||||
assert.deepStrictEqual(hostport.addr, tests[i].expectedAddr);
|
||||
assert.strictEqual(hostport.port, tests[i].expectedPort);
|
||||
}
|
||||
});
|
||||
});
|
||||
60
commands/controls.js
vendored
60
commands/controls.js
vendored
@ -1,60 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
*
|
||||
* @returns {object} Control object
|
||||
*
|
||||
* @throws {Exception} When given control type is undefined
|
||||
*
|
||||
*/
|
||||
get(type) {
|
||||
if (typeof this.controls[type] !== "object") {
|
||||
throw new Exception('Control "' + type + '" was undefined');
|
||||
}
|
||||
|
||||
return this.controls[type];
|
||||
}
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
export default class Exception extends Error {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {string} message error message
|
||||
*
|
||||
*/
|
||||
constructor(message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -1,311 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as command from "./commands.js";
|
||||
|
||||
/**
|
||||
* Extract needed data
|
||||
*
|
||||
* @param {Array<string>} kept The keys of of the data to be kept
|
||||
* @param {object} input Input data
|
||||
*
|
||||
* @return {object} Extracted data
|
||||
*/
|
||||
function extractSelectedData(kept, input) {
|
||||
if (!kept || typeof kept !== "object" || kept.length < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let data = {},
|
||||
length = 0;
|
||||
|
||||
for (let k in kept) {
|
||||
if (!input[kept[k]]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
data[kept[k]] = input[kept[k]];
|
||||
length++;
|
||||
}
|
||||
|
||||
if (length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} data to be searched
|
||||
* @param {string} metaName name of the meta
|
||||
* @param {string} valContains target string to search
|
||||
*
|
||||
*/
|
||||
function metaContains(data, metaName, valContains) {
|
||||
switch (typeof data[metaName]) {
|
||||
case "string":
|
||||
return data[metaName].indexOf(valContains) >= 0;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given uname, or -1 when not found
|
||||
*
|
||||
* @param {string} uname the unique name
|
||||
*
|
||||
* @returns {integer} The index of given uname
|
||||
*
|
||||
*/
|
||||
indexOf(uname) {
|
||||
for (let i in this.records) {
|
||||
if (this.records[i].uname !== uname) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {object} sessionData Data which only available for current session
|
||||
* @param {Array<string>} keptSessions Keys of the session data that should
|
||||
* be saved
|
||||
*
|
||||
*/
|
||||
save(uname, title, lastUsed, info, data, sessionData, keptSessions) {
|
||||
const unameIdx = this.indexOf(uname);
|
||||
|
||||
if (unameIdx >= 0) {
|
||||
this.records.splice(unameIdx, 1);
|
||||
}
|
||||
|
||||
this.records.push({
|
||||
uname: uname,
|
||||
title: title,
|
||||
type: info.name(),
|
||||
color: info.color(),
|
||||
last: lastUsed.getTime(),
|
||||
data: data,
|
||||
session: sessionData,
|
||||
keptSessions: keptSessions,
|
||||
});
|
||||
|
||||
if (this.records.length > this.maxItems) {
|
||||
this.records = this.records.slice(
|
||||
this.records.length - this.maxItems,
|
||||
this.records.length,
|
||||
);
|
||||
}
|
||||
|
||||
this.store();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current records to storage
|
||||
*
|
||||
*/
|
||||
store() {
|
||||
this.saver(this, this.export());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete record from 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear session data
|
||||
*
|
||||
* @param {string} uid unique name
|
||||
*
|
||||
*/
|
||||
clearSession(uid) {
|
||||
for (let i in this.records) {
|
||||
if (this.records[i].uname !== uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.records[i].session = null;
|
||||
this.records[i].keptSessions = [];
|
||||
break;
|
||||
}
|
||||
|
||||
this.store();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all history records. The exported data is differ than the
|
||||
* internal ones, it cannot be directly import back
|
||||
*
|
||||
* @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,
|
||||
session: this.records[i].session,
|
||||
keptSessions: this.records[i].keptSessions,
|
||||
});
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export current history records
|
||||
*
|
||||
* @returns {Array<object>} Records
|
||||
*
|
||||
*/
|
||||
export() {
|
||||
let r = [];
|
||||
|
||||
for (let i in this.records) {
|
||||
r.push({
|
||||
uname: this.records[i].uname,
|
||||
title: this.records[i].title,
|
||||
type: this.records[i].type,
|
||||
color: this.records[i].color,
|
||||
last: this.records[i].last,
|
||||
data: this.records[i].data,
|
||||
session: extractSelectedData(
|
||||
this.records[i].keptSessions,
|
||||
this.records[i].session,
|
||||
),
|
||||
keptSessions: this.records[i].keptSessions,
|
||||
});
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import data into current history records
|
||||
*
|
||||
* @param {Array<object>} records Records
|
||||
*
|
||||
*/
|
||||
import(records) {
|
||||
for (let i in records) {
|
||||
if (this.indexOf(records[i].uname) >= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.records.push({
|
||||
uname: records[i].uname,
|
||||
title: records[i].title,
|
||||
type: records[i].type,
|
||||
color: records[i].color,
|
||||
last: records[i].last,
|
||||
data: records[i].data,
|
||||
session: extractSelectedData(
|
||||
records[i].keptSessions,
|
||||
records[i].session,
|
||||
),
|
||||
keptSessions: records[i].keptSessions,
|
||||
});
|
||||
}
|
||||
|
||||
this.store();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for partly matched results
|
||||
*
|
||||
* @param {string} type of the history record
|
||||
* @param {string} metaName name of the meta data
|
||||
* @param {string} keyword keyword to search
|
||||
* @param {number} max max results
|
||||
*/
|
||||
search(type, metaName, keyword, max) {
|
||||
let maxResults = max > this.records.length ? this.records.length : max;
|
||||
let s = [];
|
||||
|
||||
if (maxResults < 0) {
|
||||
maxResults = this.records.length;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.records.length && s.length < maxResults; i++) {
|
||||
if (this.records[i].type !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.records[i].data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!metaContains(this.records[i].data, metaName, keyword)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
s.push(this.records[i]);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as reader from "../stream/reader.js";
|
||||
import Exception from "./exception.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;
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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.strictEqual(marshalled.length, 1);
|
||||
|
||||
r.feed(marshalled);
|
||||
|
||||
let i2 = new integer.Integer(0);
|
||||
|
||||
await i2.unmarshal(r);
|
||||
|
||||
assert.strictEqual(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.strictEqual(marshalled.length, 2);
|
||||
|
||||
r.feed(marshalled);
|
||||
|
||||
let i2 = new integer.Integer(0);
|
||||
|
||||
await i2.unmarshal(r);
|
||||
|
||||
assert.strictEqual(i.value(), i2.value());
|
||||
});
|
||||
});
|
||||
@ -1,327 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
|
||||
/**
|
||||
* Default preset item, contains data of a default preset
|
||||
*
|
||||
*/
|
||||
const presetItem = {
|
||||
title: "",
|
||||
type: "",
|
||||
host: "",
|
||||
meta: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify Preset Item Meta
|
||||
*
|
||||
* @param {object} preset
|
||||
*
|
||||
*/
|
||||
function verifyPresetItemMeta(preset) {
|
||||
for (let i in preset.meta) {
|
||||
if (typeof preset.meta[i] === "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'The data type of meta field "' +
|
||||
i +
|
||||
'" was "' +
|
||||
typeof preset.meta[i] +
|
||||
'" instead of expected "string"',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and verify the given preset, return a valid preset
|
||||
*
|
||||
* @param {object} item
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
* @return {object}
|
||||
*
|
||||
*/
|
||||
function parsePresetItem(item) {
|
||||
let preset = {};
|
||||
|
||||
for (let i in presetItem) {
|
||||
preset[i] = presetItem[i];
|
||||
}
|
||||
|
||||
for (let i in presetItem) {
|
||||
if (typeof presetItem[i] === typeof item[i]) {
|
||||
preset[i] = item[i];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'Expecting the data type of "' +
|
||||
i +
|
||||
'" is "' +
|
||||
typeof presetItem[i] +
|
||||
'", given "' +
|
||||
typeof item[i] +
|
||||
'" instead',
|
||||
);
|
||||
}
|
||||
|
||||
verifyPresetItemMeta(preset.meta);
|
||||
|
||||
return preset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset data
|
||||
*
|
||||
*/
|
||||
export class Preset {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} preset preset data
|
||||
*
|
||||
*/
|
||||
constructor(preset) {
|
||||
this.preset = parsePresetItem(preset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the title of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
title() {
|
||||
return this.preset.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
type() {
|
||||
return this.preset.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the host of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
host() {
|
||||
return this.preset.host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the given meta of current preset
|
||||
*
|
||||
* @param {string} name name of the meta data
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
meta(name) {
|
||||
if (typeof this.preset.meta[name] !== "string") {
|
||||
throw new Exception('Meta "' + name + '" was undefined');
|
||||
}
|
||||
|
||||
return this.preset.meta[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the given meta of current preset, and if failed, return the given
|
||||
* default value
|
||||
*
|
||||
* @param {string} name name of the meta data
|
||||
* @param {string} defaultValue default value to be returned when the meta was
|
||||
* not found
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
metaDefault(name, defaultValue) {
|
||||
try {
|
||||
return this.meta(name);
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new meta item
|
||||
*
|
||||
* @param {string} name name of the meta data
|
||||
* @param {string} data data of the meta data
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
*/
|
||||
insertMeta(name, data) {
|
||||
if (typeof this.preset.meta[name] !== "undefined") {
|
||||
throw new Exception('Meta "' + name + '" has already been defined');
|
||||
}
|
||||
|
||||
this.preset.meta[name] = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all meta keys
|
||||
*
|
||||
* @returns {Array<string>} All meta keys
|
||||
*
|
||||
*/
|
||||
metaKeys() {
|
||||
let keys = [];
|
||||
|
||||
for (let k in this.preset.meta) {
|
||||
keys.push(k);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty preset
|
||||
*
|
||||
* @returns {Preset}
|
||||
*
|
||||
*/
|
||||
export function emptyPreset() {
|
||||
return new Preset({
|
||||
title: "Default",
|
||||
type: "Default",
|
||||
host: "",
|
||||
meta: {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Command Preset manager
|
||||
*
|
||||
*/
|
||||
export class Presets {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Array<object>} presets Array of preset data
|
||||
*
|
||||
*/
|
||||
constructor(presets) {
|
||||
this.presets = [];
|
||||
|
||||
for (let i = 0; i < presets.length; i++) {
|
||||
this.presets.push(new Preset(presets[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all presets of a type
|
||||
*
|
||||
* @param {string} type type of the presets data
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
fetch(type) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return presets with matched type and meta data
|
||||
*
|
||||
* @param {string} type type of the presets data
|
||||
* @param {string} metaName name of the meta data
|
||||
* @param {string} metaVal value of the meta data
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
meta(type, metaName, metaVal) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.presets[i].meta(metaName) !== metaVal) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof Exception)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return presets with matched type and host
|
||||
*
|
||||
* @param {string} type type of the presets
|
||||
* @param {string} host host of the presets
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
hosts(type, host) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.presets[i].host() !== host) {
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
}
|
||||
1124
commands/ssh.js
1124
commands/ssh.js
File diff suppressed because it is too large
Load Diff
@ -1,72 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,265 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import assert from "assert";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as strings from "./string.js";
|
||||
|
||||
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.deepStrictEqual(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.deepStrictEqual(s2.data(), s.data());
|
||||
});
|
||||
});
|
||||
@ -1,648 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as header from "../stream/header.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as address from "./address.js";
|
||||
import * as command from "./commands.js";
|
||||
import * as common from "./common.js";
|
||||
import * as controls from "./controls.js";
|
||||
import * as event from "./events.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as history from "./history.js";
|
||||
import * as presets from "./presets.js";
|
||||
|
||||
const COMMAND_ID = 0x00;
|
||||
|
||||
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;
|
||||
|
||||
const HostMaxSearchResults = 3;
|
||||
|
||||
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.sendData(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",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
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";
|
||||
},
|
||||
},
|
||||
Encoding: {
|
||||
name: "Encoding",
|
||||
description: "The character encoding of the server",
|
||||
type: "select",
|
||||
value: "utf-8",
|
||||
example: common.charsetPresets.join(","),
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
for (let i in common.charsetPresets) {
|
||||
if (common.charsetPresets[i] !== d) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
throw new Error('The character encoding "' + d + '" is not supported');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {Array<string>} keptSessions
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
info,
|
||||
preset,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
) {
|
||||
this.info = info;
|
||||
this.preset = preset;
|
||||
this.hasStarted = false;
|
||||
this.streams = streams;
|
||||
this.session = session;
|
||||
this.keptSessions = keptSessions;
|
||||
this.step = subs;
|
||||
this.controls = controls.get("Telnet");
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.step.resolve(this.stepInitialPrompt());
|
||||
}
|
||||
|
||||
started() {
|
||||
return this.hasStarted;
|
||||
}
|
||||
|
||||
control() {
|
||||
return this.controls;
|
||||
}
|
||||
|
||||
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 the 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
|
||||
* @param {object} sessionData
|
||||
*
|
||||
*/
|
||||
buildCommand(sender, configInput, sessionData) {
|
||||
let self = this;
|
||||
|
||||
let parsedConfig = {
|
||||
host: address.parseHostPort(configInput.host, DEFAULT_PORT),
|
||||
charset: configInput.charset,
|
||||
};
|
||||
|
||||
// Copy the keptSessions from the record so it will not be overwritten here
|
||||
let keptSessions = self.keptSessions ? [].concat(...self.keptSessions) : [];
|
||||
|
||||
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.build({
|
||||
charset: parsedConfig.charset,
|
||||
send(data) {
|
||||
return commandHandler.sendData(data);
|
||||
},
|
||||
close() {
|
||||
return commandHandler.sendClose();
|
||||
},
|
||||
events: commandHandler.events,
|
||||
}),
|
||||
self.controls.ui(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
self.history.save(
|
||||
self.info.name() + ":" + configInput.host,
|
||||
configInput.host,
|
||||
new Date(),
|
||||
self.info,
|
||||
configInput,
|
||||
sessionData,
|
||||
keptSessions,
|
||||
);
|
||||
},
|
||||
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() {
|
||||
const self = this;
|
||||
|
||||
return command.prompt(
|
||||
"Telnet",
|
||||
"Teletype Network",
|
||||
"Connect",
|
||||
(r) => {
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, (sd) => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
host: r.host,
|
||||
charset: r.encoding,
|
||||
},
|
||||
self.session,
|
||||
);
|
||||
});
|
||||
|
||||
self.step.resolve(self.stepWaitForAcceptWait());
|
||||
},
|
||||
() => {},
|
||||
command.fieldsWithPreset(
|
||||
initialFieldDef,
|
||||
[
|
||||
{
|
||||
name: "Host",
|
||||
suggestions(input) {
|
||||
const hosts = self.history.search(
|
||||
"Telnet",
|
||||
"host",
|
||||
input,
|
||||
HostMaxSearchResults,
|
||||
);
|
||||
|
||||
let sugg = [];
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
sugg.push({
|
||||
title: hosts[i].title,
|
||||
value: hosts[i].data.host,
|
||||
meta: {
|
||||
Encoding: hosts[i].data.charset,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return sugg;
|
||||
},
|
||||
},
|
||||
{ name: "Encoding" },
|
||||
],
|
||||
self.preset,
|
||||
(r) => {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Executor extends Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {object} config
|
||||
* @param {object} session
|
||||
* @param {Array<string>} keptSessions
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
info,
|
||||
config,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
) {
|
||||
super(
|
||||
info,
|
||||
presets.emptyPreset(),
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
);
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
stepInitialPrompt() {
|
||||
const self = this;
|
||||
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, (sd) => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
host: self.config.host,
|
||||
charset: self.config.charset ? self.config.charset : "utf-8",
|
||||
},
|
||||
self.session,
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
}
|
||||
|
||||
export class Command {
|
||||
constructor() {}
|
||||
|
||||
id() {
|
||||
return COMMAND_ID;
|
||||
}
|
||||
|
||||
name() {
|
||||
return "Telnet";
|
||||
}
|
||||
|
||||
description() {
|
||||
return "Teletype Network";
|
||||
}
|
||||
|
||||
color() {
|
||||
return "#6ac";
|
||||
}
|
||||
|
||||
wizard(
|
||||
info,
|
||||
preset,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
) {
|
||||
return new Wizard(
|
||||
info,
|
||||
preset,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
);
|
||||
}
|
||||
|
||||
execute(
|
||||
info,
|
||||
config,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
) {
|
||||
return new Executor(
|
||||
info,
|
||||
config,
|
||||
session,
|
||||
keptSessions,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
);
|
||||
}
|
||||
|
||||
launch(info, launcher, streams, subs, controls, history) {
|
||||
const d = launcher.split("|", 2);
|
||||
|
||||
if (d.length <= 0) {
|
||||
throw new Exception('Given launcher "' + launcher + '" was invalid');
|
||||
}
|
||||
|
||||
try {
|
||||
initialFieldDef["Host"].verify(d[0]);
|
||||
} catch (e) {
|
||||
throw new Exception(
|
||||
'Given launcher "' + launcher + '" was invalid: ' + e,
|
||||
);
|
||||
}
|
||||
|
||||
let charset = "utf-8";
|
||||
|
||||
if (d.length > 1) {
|
||||
// TODO: Remove this check after depreciation period.
|
||||
try {
|
||||
initialFieldDef["Encoding"].verify(d[1]);
|
||||
|
||||
charset = d[1];
|
||||
} catch (e) {
|
||||
throw new Exception(
|
||||
'Given launcher "' + launcher + '" was invalid: ' + e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.execute(
|
||||
info,
|
||||
{
|
||||
host: d[0],
|
||||
charset: charset,
|
||||
},
|
||||
null,
|
||||
null,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history,
|
||||
);
|
||||
}
|
||||
|
||||
launcher(config) {
|
||||
return config.host + "|" + (config.charset ? config.charset : "utf-8");
|
||||
}
|
||||
|
||||
represet(preset) {
|
||||
const host = preset.host();
|
||||
|
||||
if (host.length > 0) {
|
||||
preset.insertMeta("Host", host);
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
152
control/ssh.js
152
control/ssh.js
@ -1,152 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as iconv from "iconv-lite";
|
||||
import * as color from "../commands/color.js";
|
||||
import * as common from "../commands/common.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as subscribe from "../stream/subscribe.js";
|
||||
|
||||
class Control {
|
||||
constructor(data, color) {
|
||||
this.colorM = color;
|
||||
this.colors = this.colorM.get();
|
||||
this.charset = data.charset;
|
||||
|
||||
this.charsetDecoder = (d) => {
|
||||
return iconv.decode(d, this.charset);
|
||||
};
|
||||
this.charsetEncoder = (dStr) => {
|
||||
return iconv.encode(dStr, this.charset);
|
||||
};
|
||||
|
||||
this.enable = false;
|
||||
this.sender = data.send;
|
||||
this.closer = data.close;
|
||||
this.resizer = data.resize;
|
||||
this.subs = new subscribe.Subscribe();
|
||||
|
||||
let self = this;
|
||||
|
||||
data.events.place("stdout", async (rd) => {
|
||||
try {
|
||||
self.subs.resolve(self.charsetDecoder(await reader.readCompletely(rd)));
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
|
||||
data.events.place("stderr", async (rd) => {
|
||||
try {
|
||||
self.subs.resolve(self.charsetDecoder(await reader.readCompletely(rd)));
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
|
||||
data.events.place("completed", () => {
|
||||
self.closed = true;
|
||||
self.colorM.forget(self.colors.color);
|
||||
|
||||
self.subs.reject("Remote connection has been terminated");
|
||||
});
|
||||
}
|
||||
|
||||
echo() {
|
||||
return false;
|
||||
}
|
||||
|
||||
resize(dim) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resizer(dim.rows, dim.cols);
|
||||
}
|
||||
|
||||
enabled() {
|
||||
this.enable = true;
|
||||
}
|
||||
|
||||
disabled() {
|
||||
this.enable = false;
|
||||
}
|
||||
|
||||
retap(isOn) {}
|
||||
|
||||
receive() {
|
||||
return this.subs.subscribe();
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sender(this.charsetEncoder(data));
|
||||
}
|
||||
|
||||
sendBinary(data) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sender(common.strToBinary(data));
|
||||
}
|
||||
|
||||
color() {
|
||||
return this.colors.dark;
|
||||
}
|
||||
|
||||
activeColor() {
|
||||
return this.colors.color;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.closer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cc = this.closer;
|
||||
this.closer = null;
|
||||
|
||||
return cc();
|
||||
}
|
||||
}
|
||||
|
||||
export class SSH {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {color.Color} c
|
||||
*/
|
||||
constructor(c) {
|
||||
this.color = c;
|
||||
}
|
||||
|
||||
type() {
|
||||
return "SSH";
|
||||
}
|
||||
|
||||
ui() {
|
||||
return "Console";
|
||||
}
|
||||
|
||||
build(data) {
|
||||
return new Control(data, this.color);
|
||||
}
|
||||
}
|
||||
@ -1,513 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as iconv from "iconv-lite";
|
||||
import * as color from "../commands/color.js";
|
||||
import * as common from "../commands/common.js";
|
||||
import Exception from "../commands/exception.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as subscribe from "../stream/subscribe.js";
|
||||
|
||||
// const maxReadBufSize = 1024;
|
||||
|
||||
const cmdSE = 240;
|
||||
// const cmdNOP = 241;
|
||||
// const cmdDataMark = 242;
|
||||
// const cmdBreak = 243;
|
||||
// const cmdInterrputProcess = 244;
|
||||
// const cmdAbortOutput = 245;
|
||||
// const cmdAreYouThere = 246;
|
||||
// const cmdEraseCharacter = 247;
|
||||
// const cmdEraseLine = 248;
|
||||
const cmdGoAhead = 249;
|
||||
const cmdSB = 250;
|
||||
const cmdWill = 251;
|
||||
const cmdWont = 252;
|
||||
const cmdDo = 253;
|
||||
const cmdDont = 254;
|
||||
const cmdIAC = 255;
|
||||
|
||||
const optEcho = 1;
|
||||
const optSuppressGoAhead = 3;
|
||||
const optTerminalType = 24;
|
||||
const optNAWS = 31;
|
||||
|
||||
const optTerminalTypeIs = 0;
|
||||
const optTerminalTypeSend = 1;
|
||||
|
||||
const unknownTermTypeSendData = new Uint8Array([
|
||||
optTerminalTypeIs,
|
||||
88,
|
||||
84,
|
||||
69,
|
||||
82,
|
||||
77,
|
||||
]);
|
||||
|
||||
// Most of code of this class is directly from
|
||||
// https://github.com/ziutek/telnet/blob/master/conn.go#L122
|
||||
// Thank you!
|
||||
class Parser {
|
||||
constructor(sender, flusher, callbacks) {
|
||||
this.sender = sender;
|
||||
this.flusher = flusher;
|
||||
this.callbacks = callbacks;
|
||||
this.reader = new reader.Multiple(() => {});
|
||||
this.options = {
|
||||
echoEnabled: false,
|
||||
suppressGoAhead: false,
|
||||
nawsAccpeted: false,
|
||||
};
|
||||
this.current = 0;
|
||||
}
|
||||
|
||||
sendNego(cmd, option) {
|
||||
return this.sender(new Uint8Array([cmdIAC, cmd, option]));
|
||||
}
|
||||
|
||||
sendDeny(cmd, o) {
|
||||
switch (cmd) {
|
||||
case cmdDo:
|
||||
return this.sendNego(cmdWont, o);
|
||||
|
||||
case (cmdWill, cmdWont):
|
||||
return this.sendNego(cmdDont, o);
|
||||
}
|
||||
}
|
||||
|
||||
sendWillSubNego(willCmd, data, option) {
|
||||
let b = new Uint8Array(6 + data.length + 2);
|
||||
|
||||
b.set([cmdIAC, willCmd, option, cmdIAC, cmdSB, option], 0);
|
||||
b.set(data, 6);
|
||||
b.set([cmdIAC, cmdSE], data.length + 6);
|
||||
|
||||
return this.sender(b);
|
||||
}
|
||||
|
||||
sendSubNego(data, option) {
|
||||
let b = new Uint8Array(3 + data.length + 2);
|
||||
|
||||
b.set([cmdIAC, cmdSB, option], 0);
|
||||
b.set(data, 3);
|
||||
b.set([cmdIAC, cmdSE], data.length + 3);
|
||||
|
||||
return this.sender(b);
|
||||
}
|
||||
|
||||
async handleTermTypeSubNego(rd) {
|
||||
let action = await reader.readOne(rd);
|
||||
|
||||
if (action[0] !== optTerminalTypeSend) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
return () => {
|
||||
self.sendSubNego(unknownTermTypeSendData, optTerminalType);
|
||||
};
|
||||
}
|
||||
|
||||
async handleSubNego(rd) {
|
||||
let endExec = null;
|
||||
|
||||
for (;;) {
|
||||
let d = await reader.readOne(rd);
|
||||
|
||||
switch (d[0]) {
|
||||
case optTerminalType:
|
||||
endExec = await this.handleTermTypeSubNego(rd);
|
||||
continue;
|
||||
|
||||
case cmdIAC:
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
let e = await reader.readOne(rd);
|
||||
|
||||
if (e[0] !== cmdSE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (endExec !== null) {
|
||||
endExec();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleOption(cmd, option, oldVal, newVal) {
|
||||
switch (cmd) {
|
||||
case cmdWill:
|
||||
if (!oldVal) {
|
||||
this.sendNego(cmdDo, option);
|
||||
}
|
||||
|
||||
newVal(true, cmdWill);
|
||||
return;
|
||||
|
||||
case cmdWont:
|
||||
if (oldVal) {
|
||||
this.sendNego(cmdDont, option);
|
||||
}
|
||||
|
||||
newVal(false, cmdWont);
|
||||
return;
|
||||
|
||||
case cmdDo:
|
||||
if (!oldVal) {
|
||||
this.sendNego(cmdWill, option);
|
||||
}
|
||||
|
||||
newVal(true, cmdDo);
|
||||
return;
|
||||
|
||||
case cmdDont:
|
||||
if (oldVal) {
|
||||
this.sendNego(cmdWont, option);
|
||||
}
|
||||
|
||||
newVal(false, cmdDont);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async handleCmd(rd) {
|
||||
let d = await reader.readOne(rd);
|
||||
|
||||
switch (d[0]) {
|
||||
case cmdWill:
|
||||
case cmdWont:
|
||||
case cmdDo:
|
||||
case cmdDont:
|
||||
break;
|
||||
|
||||
case cmdIAC:
|
||||
this.flusher(d);
|
||||
return;
|
||||
|
||||
case cmdGoAhead:
|
||||
return;
|
||||
|
||||
case cmdSB:
|
||||
await this.handleSubNego(rd);
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown command");
|
||||
}
|
||||
|
||||
let o = await reader.readOne(rd);
|
||||
|
||||
switch (o[0]) {
|
||||
case optEcho:
|
||||
return this.handleOption(
|
||||
d[0],
|
||||
o[0],
|
||||
this.options.echoEnabled,
|
||||
(d, action) => {
|
||||
this.options.echoEnabled = d;
|
||||
|
||||
switch (action) {
|
||||
case cmdWill:
|
||||
case cmdDont:
|
||||
this.callbacks.setEcho(false);
|
||||
break;
|
||||
|
||||
case cmdWont:
|
||||
case cmdDo:
|
||||
this.callbacks.setEcho(true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
case optSuppressGoAhead:
|
||||
return this.handleOption(
|
||||
d[0],
|
||||
o[0],
|
||||
this.options.suppressGoAhead,
|
||||
(d, _action) => {
|
||||
this.options.suppressGoAhead = d;
|
||||
},
|
||||
);
|
||||
|
||||
case optNAWS:
|
||||
// Window resize allowed?
|
||||
if (d[0] !== cmdDo) {
|
||||
this.sendDeny(d[0], o[0]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
let dim = this.callbacks.getWindowDim(),
|
||||
dimData = new DataView(new ArrayBuffer(4));
|
||||
|
||||
dimData.setUint16(0, dim.cols);
|
||||
dimData.setUint16(2, dim.rows);
|
||||
|
||||
let dimBytes = new Uint8Array(dimData.buffer);
|
||||
|
||||
if (this.options.nawsAccpeted) {
|
||||
this.sendSubNego(dimBytes, optNAWS);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.options.nawsAccpeted = true;
|
||||
this.sendWillSubNego(cmdWill, dimBytes, optNAWS);
|
||||
}
|
||||
return;
|
||||
|
||||
case optTerminalType:
|
||||
if (d[0] !== cmdDo) {
|
||||
this.sendDeny(d[0], o[0]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendNego(cmdWill, o[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendDeny(d[0], o[0]);
|
||||
}
|
||||
|
||||
requestWindowResize() {
|
||||
this.options.nawsAccpeted = true;
|
||||
|
||||
this.sendNego(cmdWill, optNAWS);
|
||||
}
|
||||
|
||||
async run() {
|
||||
try {
|
||||
for (;;) {
|
||||
let d = await reader.readUntil(this.reader, cmdIAC);
|
||||
|
||||
if (!d.found) {
|
||||
this.flusher(d.data);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d.data.length > 1) {
|
||||
this.flusher(d.data.slice(0, d.data.length - 1));
|
||||
}
|
||||
|
||||
await this.handleCmd(this.reader);
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
feed(rd, cb) {
|
||||
this.reader.feed(rd, cb);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
class Control {
|
||||
constructor(data, color) {
|
||||
this.colorM = color;
|
||||
this.colors = this.colorM.get();
|
||||
this.charset = data.charset;
|
||||
|
||||
this.charsetDecoder = (d) => {
|
||||
return iconv.decode(d, this.charset);
|
||||
};
|
||||
this.charsetEncoder = (dStr) => {
|
||||
return iconv.encode(dStr, this.charset);
|
||||
};
|
||||
|
||||
this.sender = data.send;
|
||||
this.closer = data.close;
|
||||
this.closed = false;
|
||||
this.localEchoEnabled = true;
|
||||
this.subs = new subscribe.Subscribe();
|
||||
this.enable = false;
|
||||
this.windowDim = {
|
||||
cols: 65535,
|
||||
rows: 65535,
|
||||
};
|
||||
|
||||
let self = this;
|
||||
|
||||
this.parser = new Parser(
|
||||
this.sender,
|
||||
(d) => {
|
||||
self.subs.resolve(this.charsetDecoder(d));
|
||||
},
|
||||
{
|
||||
setEcho(newVal) {
|
||||
self.localEchoEnabled = newVal;
|
||||
},
|
||||
getWindowDim() {
|
||||
return self.windowDim;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let runWait = this.parser.run();
|
||||
|
||||
data.events.place("inband", (rd) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
self.parser.feed(rd, () => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
data.events.place("completed", async () => {
|
||||
self.parser.close();
|
||||
self.closed = true;
|
||||
|
||||
self.colorM.forget(self.colors.color);
|
||||
|
||||
await runWait;
|
||||
|
||||
self.subs.reject("Remote connection has been terminated");
|
||||
});
|
||||
}
|
||||
|
||||
echo() {
|
||||
return this.localEchoEnabled;
|
||||
}
|
||||
|
||||
resize(dim) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.windowDim.cols = dim.cols;
|
||||
this.windowDim.rows = dim.rows;
|
||||
|
||||
this.parser.requestWindowResize();
|
||||
}
|
||||
|
||||
enabled() {
|
||||
this.enable = true;
|
||||
}
|
||||
|
||||
disabled() {
|
||||
this.enable = false;
|
||||
}
|
||||
|
||||
retap(_isOn) {}
|
||||
|
||||
receive() {
|
||||
return this.subs.subscribe();
|
||||
}
|
||||
|
||||
searchNextIAC(start, data) {
|
||||
for (let i = start; i < data.length; i++) {
|
||||
if (data[i] !== cmdIAC) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
sendSeg(enc) {
|
||||
let currentLen = 0;
|
||||
|
||||
while (currentLen < enc.length) {
|
||||
const iacPos = this.searchNextIAC(currentLen, enc);
|
||||
|
||||
if (iacPos < 0) {
|
||||
this.sender(enc.slice(currentLen, enc.length));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.sender(enc.slice(currentLen, iacPos + 1));
|
||||
this.sender(enc.slice(iacPos, iacPos + 1));
|
||||
|
||||
currentLen = iacPos + 1;
|
||||
}
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendSeg(this.charsetEncoder(data));
|
||||
}
|
||||
|
||||
sendBinary(data) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sendSeg(common.strToBinary(data));
|
||||
}
|
||||
|
||||
color() {
|
||||
return this.colors.dark;
|
||||
}
|
||||
|
||||
activeColor() {
|
||||
return this.colors.color;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.closer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cc = this.closer;
|
||||
this.closer = null;
|
||||
|
||||
return cc();
|
||||
}
|
||||
}
|
||||
|
||||
export class Telnet {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {color.Color} c
|
||||
*/
|
||||
constructor(c) {
|
||||
this.color = c;
|
||||
}
|
||||
|
||||
type() {
|
||||
return "Telnet";
|
||||
}
|
||||
|
||||
ui() {
|
||||
return "Console";
|
||||
}
|
||||
|
||||
build(data) {
|
||||
return new Control(data, this.color);
|
||||
}
|
||||
}
|
||||
109
stream/common.js
109
stream/common.js
@ -1,109 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Get one unsafe random number
|
||||
*
|
||||
* @param {number} min Min value (included)
|
||||
* @param {number} max Max value (not included)
|
||||
*
|
||||
* @returns {number} Get random number
|
||||
*
|
||||
*/
|
||||
export function getRand(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a group of random number
|
||||
*
|
||||
* @param {number} n How many number to get
|
||||
* @param {number} min Min value (included)
|
||||
* @param {number} max Max value (not included)
|
||||
*
|
||||
* @returns {Array<number>} A group of random number
|
||||
*/
|
||||
export function getRands(n, min, max) {
|
||||
let r = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
r.push(getRand(min, max));
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate given buffer to multiple ones based on input max length
|
||||
*
|
||||
* @param {Uint8Array} buf Buffer to separate
|
||||
* @param {number} max Max length of each buffer
|
||||
*
|
||||
* @returns {Array<Uint8Array>} Separated buffers
|
||||
*
|
||||
*/
|
||||
export function separateBuffer(buf, max) {
|
||||
let start = 0,
|
||||
result = [];
|
||||
|
||||
while (start < buf.length) {
|
||||
let remain = buf.length - start;
|
||||
|
||||
if (remain <= max) {
|
||||
result.push(buf.slice(start, start + remain));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
remain = max;
|
||||
|
||||
result.push(buf.slice(start, start + remain));
|
||||
start += remain;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Uint8Array out of given binary string
|
||||
*
|
||||
* @param {string} str binary string
|
||||
*
|
||||
* @returns {Uint8Array} Separated buffers
|
||||
*
|
||||
*/
|
||||
export function buildBufferFromString(str) {
|
||||
let r = [],
|
||||
t = [];
|
||||
|
||||
for (let i in str) {
|
||||
let c = str.charCodeAt(i);
|
||||
|
||||
while (c > 0xff) {
|
||||
t.push(c & 0xff);
|
||||
c >>= 8;
|
||||
}
|
||||
|
||||
r.push(c);
|
||||
|
||||
for (let j = t.length; j > 0; j--) {
|
||||
r.push(t[j]);
|
||||
}
|
||||
|
||||
t = [];
|
||||
}
|
||||
|
||||
return new Uint8Array(r);
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import assert from "assert";
|
||||
import * as common from "./common.js";
|
||||
|
||||
describe("Common", () => {
|
||||
it("separateBuffer", async () => {
|
||||
let resultArr = [];
|
||||
const expected = new Uint8Array([
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3,
|
||||
4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7,
|
||||
8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1,
|
||||
2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5,
|
||||
6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
||||
]),
|
||||
sepSeg = common.separateBuffer(expected, 16);
|
||||
|
||||
sepSeg.forEach((d) => {
|
||||
resultArr.push(...d);
|
||||
});
|
||||
|
||||
const result = new Uint8Array(resultArr);
|
||||
|
||||
assert.deepStrictEqual(result, expected);
|
||||
});
|
||||
});
|
||||
@ -1,31 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
export default class Exception extends Error {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {string} message error message
|
||||
* @param {boolean} temporary whether or not the error is temporary
|
||||
*
|
||||
*/
|
||||
constructor(message, temporary) {
|
||||
super(message);
|
||||
|
||||
this.temporary = temporary;
|
||||
}
|
||||
}
|
||||
264
stream/header.js
264
stream/header.js
@ -1,264 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
|
||||
export const CONTROL = 0x00;
|
||||
export const STREAM = 0x40;
|
||||
export const CLOSE = 0x80;
|
||||
export const COMPLETED = 0xc0;
|
||||
|
||||
export const CONTROL_ECHO = 0x00;
|
||||
export const CONTROL_PAUSESTREAM = 0x01;
|
||||
export const CONTROL_RESUMESTREAM = 0x02;
|
||||
|
||||
const headerHeaderCutter = 0xc0;
|
||||
const headerDataCutter = 0x3f;
|
||||
|
||||
export const HEADER_MAX_DATA = headerDataCutter;
|
||||
|
||||
export class Header {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} headerByte one byte data of the header
|
||||
*/
|
||||
constructor(headerByte) {
|
||||
this.headerByte = headerByte;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header type
|
||||
*
|
||||
* @returns {number} Type number
|
||||
*
|
||||
*/
|
||||
type() {
|
||||
return this.headerByte & headerHeaderCutter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header data
|
||||
*
|
||||
* @returns {number} Data number
|
||||
*
|
||||
*/
|
||||
data() {
|
||||
return this.headerByte & headerDataCutter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reader data
|
||||
*
|
||||
* @param {number} data
|
||||
*/
|
||||
set(data) {
|
||||
if (data > headerDataCutter) {
|
||||
throw new Exception("data must not be greater than 0x3f", false);
|
||||
}
|
||||
|
||||
this.headerByte |= headerDataCutter & data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header value
|
||||
*
|
||||
* @returns {number} Header byte data
|
||||
*
|
||||
*/
|
||||
value() {
|
||||
return this.headerByte;
|
||||
}
|
||||
}
|
||||
|
||||
export const STREAM_HEADER_BYTE_LENGTH = 2;
|
||||
export const STREAM_MAX_LENGTH = 0x1fff;
|
||||
export const STREAM_MAX_MARKER = 0x07;
|
||||
|
||||
const streamHeaderLengthFirstByteCutter = 0x1f;
|
||||
|
||||
export class Stream {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} headerByte1 First header byte
|
||||
* @param {number} headerByte2 Second header byte
|
||||
*
|
||||
*/
|
||||
constructor(headerByte1, headerByte2) {
|
||||
this.headerByte1 = headerByte1;
|
||||
this.headerByte2 = headerByte2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the marker data
|
||||
*
|
||||
* @returns {number} the marker
|
||||
*
|
||||
*/
|
||||
marker() {
|
||||
return this.headerByte1 >> 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stream data length
|
||||
*
|
||||
* @returns {number} Length of the stream data
|
||||
*
|
||||
*/
|
||||
length() {
|
||||
let r = 0;
|
||||
|
||||
r |= this.headerByte1 & streamHeaderLengthFirstByteCutter;
|
||||
r <<= 8;
|
||||
r |= this.headerByte2;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the header
|
||||
*
|
||||
* @param {number} marker Header marker
|
||||
* @param {number} length Stream data length
|
||||
*
|
||||
*/
|
||||
set(marker, length) {
|
||||
if (marker > STREAM_MAX_MARKER) {
|
||||
throw new Exception("marker must not be greater than 0x07", false);
|
||||
}
|
||||
|
||||
if (length > STREAM_MAX_LENGTH) {
|
||||
throw new Exception("n must not be greater than 0x1fff", false);
|
||||
}
|
||||
|
||||
this.headerByte1 =
|
||||
(marker << 5) | ((length >> 8) & streamHeaderLengthFirstByteCutter);
|
||||
this.headerByte2 = length & 0xff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the header data
|
||||
*
|
||||
* @returns {Uint8Array} Header data
|
||||
*
|
||||
*/
|
||||
buffer() {
|
||||
return new Uint8Array([this.headerByte1, this.headerByte2]);
|
||||
}
|
||||
}
|
||||
|
||||
export class InitialStream extends Stream {
|
||||
/**
|
||||
* Return how large the data can be
|
||||
*
|
||||
* @returns {number} Max data size
|
||||
*
|
||||
*/
|
||||
static maxDataSize() {
|
||||
return 0x07ff;
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} headerByte1 First header byte
|
||||
* @param {number} headerByte2 Second header byte
|
||||
*
|
||||
*/
|
||||
constructor(headerByte1, headerByte2) {
|
||||
super(headerByte1, headerByte2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return command ID
|
||||
*
|
||||
* @returns {number} Command ID
|
||||
*
|
||||
*/
|
||||
command() {
|
||||
return this.headerByte1 >> 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data
|
||||
*
|
||||
* @returns {number} Data
|
||||
*
|
||||
*/
|
||||
data() {
|
||||
let r = 0;
|
||||
|
||||
r |= this.headerByte1 & 0x07;
|
||||
r <<= 8;
|
||||
r |= this.headerByte2 & 0xff;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the respond is success
|
||||
*
|
||||
* @returns {boolean} True when the request is successful, false otherwise
|
||||
*
|
||||
*/
|
||||
success() {
|
||||
return (this.headerByte1 & 0x08) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the header
|
||||
*
|
||||
* @param {number} commandID Command ID
|
||||
* @param {number} data Stream data
|
||||
* @param {boolean} success Whether or not the request is successful
|
||||
*
|
||||
*/
|
||||
set(commandID, data, success) {
|
||||
if (commandID > 0x0f) {
|
||||
throw new Exception("Command ID must not greater than 0x0f", false);
|
||||
}
|
||||
|
||||
if (data > InitialStream.maxDataSize()) {
|
||||
throw new Exception("Data must not greater than 0x07ff", false);
|
||||
}
|
||||
|
||||
let dd = data & InitialStream.maxDataSize();
|
||||
|
||||
if (success) {
|
||||
dd |= 0x0800;
|
||||
}
|
||||
|
||||
this.headerByte1 = 0;
|
||||
this.headerByte1 |= commandID << 4;
|
||||
this.headerByte1 |= dd >> 8;
|
||||
this.headerByte2 = 0;
|
||||
this.headerByte2 |= dd & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new Header
|
||||
*
|
||||
* @param {number} h Header number
|
||||
*
|
||||
* @returns {Header} The header which been built
|
||||
*
|
||||
*/
|
||||
export function header(h) {
|
||||
return new Header(h);
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import assert from "assert";
|
||||
import * as header from "./header.js";
|
||||
|
||||
describe("Header", () => {
|
||||
it("Header", () => {
|
||||
let h = new header.Header(header.ECHO);
|
||||
|
||||
h.set(63);
|
||||
|
||||
let n = new header.Header(h.value());
|
||||
|
||||
assert.strictEqual(h.type(), n.type());
|
||||
assert.strictEqual(h.data(), n.data());
|
||||
assert.strictEqual(n.type(), header.CONTROL);
|
||||
assert.strictEqual(n.data(), 63);
|
||||
});
|
||||
|
||||
it("Stream", () => {
|
||||
let h = new header.Stream(0, 0);
|
||||
|
||||
h.set(header.STREAM_MAX_MARKER, header.STREAM_MAX_LENGTH);
|
||||
|
||||
assert.strictEqual(h.marker(), header.STREAM_MAX_MARKER);
|
||||
assert.strictEqual(h.length(), header.STREAM_MAX_LENGTH);
|
||||
|
||||
assert.strictEqual(h.headerByte1, 0xff);
|
||||
assert.strictEqual(h.headerByte2, 0xff);
|
||||
});
|
||||
|
||||
it("InitialStream", () => {
|
||||
let h = new header.InitialStream(0, 0);
|
||||
|
||||
h.set(15, 128, true);
|
||||
|
||||
assert.strictEqual(h.command(), 15);
|
||||
assert.strictEqual(h.data(), 128);
|
||||
assert.strictEqual(h.success(), true);
|
||||
});
|
||||
});
|
||||
587
stream/reader.js
587
stream/reader.js
@ -1,587 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
import * as subscribe from "./subscribe.js";
|
||||
|
||||
export class Buffer {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Uint8Array} buffer Array buffer
|
||||
* @param {function} depleted Callback that will be called when the buffer
|
||||
* is depleted
|
||||
*/
|
||||
constructor(buffer, depleted) {
|
||||
this.buffer = buffer;
|
||||
this.used = 0;
|
||||
this.onDepleted = depleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
searchBuffer(byteData, maxLen) {
|
||||
let searchLen = this.remains();
|
||||
|
||||
if (searchLen > maxLen) {
|
||||
searchLen = maxLen;
|
||||
}
|
||||
|
||||
for (let i = 0; i < searchLen; i++) {
|
||||
if (this.buffer[i + this.used] !== byteData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
indexOf(byteData) {
|
||||
return this.searchBuffer(byteData, this.remains());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes in the source + buffer is still available to be
|
||||
* read, return 0 when reader is depleted and thus can be ditched
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
remains() {
|
||||
return this.buffer.length - this.used;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes is still availale in the buffer.
|
||||
*
|
||||
* Note: This reader don't have renewable data source, so when buffer
|
||||
* depletes, the reader is done
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
buffered() {
|
||||
return this.remains();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
||||
* if current buffer is deplated
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
* @throws {Exception} When reader has been depleted
|
||||
*
|
||||
*/
|
||||
export(n) {
|
||||
let remain = this.remains();
|
||||
|
||||
if (remain <= 0) {
|
||||
throw new Exception("Reader has been depleted", false);
|
||||
}
|
||||
|
||||
if (remain > n) {
|
||||
remain = n;
|
||||
}
|
||||
|
||||
let exported = this.buffer.slice(this.used, this.used + remain);
|
||||
this.used += exported.length;
|
||||
|
||||
if (this.remains() <= 0) {
|
||||
this.onDepleted();
|
||||
}
|
||||
|
||||
return exported;
|
||||
}
|
||||
}
|
||||
|
||||
export class Multiple {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {function} depleted Callback will be called when all reader is
|
||||
* depleted
|
||||
*
|
||||
*/
|
||||
constructor(depleted) {
|
||||
this.reader = null;
|
||||
this.depleted = depleted;
|
||||
this.subscribe = new subscribe.Subscribe();
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new reader as sub reader
|
||||
*
|
||||
* @param {Buffer} reader
|
||||
* @param {function} depleted Callback that will be called when given reader
|
||||
* is depleted
|
||||
*
|
||||
* @throws {Exception} When the reader is closed
|
||||
*
|
||||
*/
|
||||
feed(reader, depleted) {
|
||||
if (this.closed) {
|
||||
throw new Exception("Reader is closed", false);
|
||||
}
|
||||
|
||||
if (this.reader === null && this.subscribe.pendings() <= 0) {
|
||||
this.reader = {
|
||||
reader: reader,
|
||||
depleted: depleted,
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscribe.resolve({
|
||||
reader: reader,
|
||||
depleted: depleted,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*
|
||||
*/
|
||||
searchBuffer(byteData, maxLen) {
|
||||
if (this.reader === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.reader.reader.searchBuffer(byteData, maxLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
indexOf(byteData) {
|
||||
return this.searchBuffer(byteData, this.buffered());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available in the buffer (How many bytes of
|
||||
* buffer is left for read before reloading from data source)
|
||||
*
|
||||
* @returns {number} How many bytes left in the current buffer
|
||||
*/
|
||||
buffered() {
|
||||
if (this.reader == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.reader.reader.buffered();
|
||||
}
|
||||
|
||||
/**
|
||||
* close current reading
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
return this.closeWithReason("Reader is closed");
|
||||
}
|
||||
|
||||
/**
|
||||
* close current reading
|
||||
*
|
||||
* @param {string} reason Reason
|
||||
*
|
||||
*/
|
||||
closeWithReason(reason) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
this.subscribe.reject(new Exception(reason, false));
|
||||
this.subscribe.disable(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
||||
* if current buffer is deplated
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
async export(n) {
|
||||
for (;;) {
|
||||
if (this.reader !== null) {
|
||||
let exported = await this.reader.reader.export(n);
|
||||
|
||||
if (this.reader.reader.remains() <= 0) {
|
||||
this.reader.depleted();
|
||||
|
||||
this.reader = null;
|
||||
}
|
||||
|
||||
return exported;
|
||||
}
|
||||
|
||||
this.depleted(this);
|
||||
|
||||
this.reader = await this.subscribe.subscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Reader {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Multiple} multiple Source reader
|
||||
* @param {function} bufferConverter Function convert
|
||||
*
|
||||
*/
|
||||
constructor(multiple, bufferConverter) {
|
||||
this.multiple = multiple;
|
||||
this.buffers = new subscribe.Subscribe();
|
||||
this.bufferConverter =
|
||||
bufferConverter ||
|
||||
((d) => {
|
||||
return d;
|
||||
});
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add buffer into current reader
|
||||
*
|
||||
* @param {Uint8Array} buffer buffer to add
|
||||
*
|
||||
* @throws {Exception} When the reader is closed
|
||||
*
|
||||
*/
|
||||
feed(buffer) {
|
||||
if (this.closed) {
|
||||
throw new Exception("Reader is closed, new data has been deined", false);
|
||||
}
|
||||
|
||||
this.buffers.resolve(buffer);
|
||||
}
|
||||
|
||||
async reader() {
|
||||
if (this.closed) {
|
||||
throw new Exception("Reader is closed, unable to read", false);
|
||||
}
|
||||
|
||||
if (this.multiple.buffered() > 0) {
|
||||
return this.multiple;
|
||||
}
|
||||
|
||||
let self = this,
|
||||
converted = await this.bufferConverter(await self.buffers.subscribe());
|
||||
|
||||
this.multiple.feed(new Buffer(converted, () => {}), () => {});
|
||||
|
||||
return this.multiple;
|
||||
}
|
||||
|
||||
/**
|
||||
* close current reading
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
return this.closeWithReason("Reader is closed");
|
||||
}
|
||||
|
||||
/**
|
||||
* close current reading
|
||||
*
|
||||
* @param {string} reason Reason
|
||||
*
|
||||
*/
|
||||
closeWithReason(reason) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
this.buffers.reject(new Exception(reason, false));
|
||||
this.buffers.disable(reason);
|
||||
|
||||
return this.multiple.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
async searchBuffer(byteData, maxLen) {
|
||||
return (await this.reader()).searchBuffer(byteData, maxLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
async indexOf(byteData) {
|
||||
return (await this.reader()).indexOf(byteData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available in the buffer (How many bytes of
|
||||
* buffer is left for read before reloading from data source)
|
||||
*
|
||||
* @returns {number} How many bytes left in the current buffer
|
||||
*/
|
||||
async buffered() {
|
||||
return (await this.reader()).buffered();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max byte length, set to 0 to refresh buffer
|
||||
* if current buffer is deplated
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
async export(n) {
|
||||
return (await this.reader()).export(n);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read exactly one bytes from the reader
|
||||
*
|
||||
* @param {Reader} reader the source reader
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
export async function readOne(reader) {
|
||||
for (;;) {
|
||||
let d = await reader.export(1);
|
||||
|
||||
if (d.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read exactly n bytes from the reader
|
||||
*
|
||||
* @param {Reader} reader the source reader
|
||||
* @param {number} n length to read
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
export async function readN(reader, n) {
|
||||
let readed = 0,
|
||||
result = new Uint8Array(n);
|
||||
|
||||
while (readed < n) {
|
||||
let exported = await reader.export(n - readed);
|
||||
|
||||
result.set(exported, readed);
|
||||
readed += exported.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export class Limited {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {Reader} reader the source reader
|
||||
* @param {number} maxN max bytes to read
|
||||
*
|
||||
* @returns {boolean} true when the reader is completed, false otherwise
|
||||
*
|
||||
*/
|
||||
constructor(reader, maxN) {
|
||||
this.reader = reader;
|
||||
this.remain = maxN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate whether or not the current reader is completed
|
||||
*
|
||||
* @returns {boolean} true when the reader is completed, false otherwise
|
||||
*
|
||||
*/
|
||||
completed() {
|
||||
return this.remain <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current available (unused) read
|
||||
* buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
* @param {number} maxLen Max search length
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*
|
||||
*/
|
||||
searchBuffer(byteData, maxLen) {
|
||||
return this.reader.searchBuffer(
|
||||
byteData,
|
||||
maxLen > this.remain ? this.remain : maxLen,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of given byte inside current read buffer
|
||||
*
|
||||
* @param {number} byteData Target data
|
||||
*
|
||||
* @returns {number} Return number >= 0 when found, -1 when not
|
||||
*/
|
||||
indexOf(byteData) {
|
||||
return this.reader.searchBuffer(byteData, this.remain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available to be read
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
remains() {
|
||||
return this.remain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how many bytes still available in the buffer (How many bytes of
|
||||
* buffer is left for read before reloading from data source)
|
||||
*
|
||||
* @returns {number} Remaining size
|
||||
*
|
||||
*/
|
||||
buffered() {
|
||||
let buf = this.reader.buffered();
|
||||
|
||||
return buf > this.remain ? this.remain : buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export max n bytes from current buffer
|
||||
*
|
||||
* @param {number} n suggested max length
|
||||
*
|
||||
* @throws {Exception} when reading already completed
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
async export(n) {
|
||||
if (this.completed()) {
|
||||
throw new Exception("Reader already completed", false);
|
||||
}
|
||||
|
||||
let toRead = n > this.remain ? this.remain : n,
|
||||
exported = await this.reader.export(toRead);
|
||||
|
||||
this.remain -= exported.length;
|
||||
|
||||
return exported;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the whole Limited reader and return the result
|
||||
*
|
||||
* @param {Limited} limited the Limited reader
|
||||
*
|
||||
* @returns {Uint8Array} Exported data
|
||||
*
|
||||
*/
|
||||
export async function readCompletely(limited) {
|
||||
return await readN(limited, limited.remains());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read until given byteData is reached. This function is guaranteed to spit
|
||||
* out at least one byte
|
||||
*
|
||||
* @param {Reader} indexOfReader
|
||||
* @param {number} byteData
|
||||
*/
|
||||
export async function readUntil(indexOfReader, byteData) {
|
||||
let pos = await indexOfReader.indexOf(byteData),
|
||||
buffered = await indexOfReader.buffered();
|
||||
|
||||
if (pos >= 0) {
|
||||
return {
|
||||
data: await readN(indexOfReader, pos + 1),
|
||||
found: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (buffered <= 0) {
|
||||
let d = await readOne(indexOfReader);
|
||||
|
||||
return {
|
||||
data: d,
|
||||
found: d[0] === byteData,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: await readN(indexOfReader, buffered),
|
||||
found: false,
|
||||
};
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import assert from "assert";
|
||||
import * as reader from "./reader.js";
|
||||
|
||||
describe("Reader", () => {
|
||||
it("Buffer", async () => {
|
||||
let buf = new reader.Buffer(
|
||||
new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
|
||||
() => {},
|
||||
);
|
||||
|
||||
let ex = buf.export(1);
|
||||
|
||||
assert.strictEqual(ex.length, 1);
|
||||
assert.strictEqual(ex[0], 0);
|
||||
assert.strictEqual(buf.remains(), 7);
|
||||
|
||||
ex = await reader.readCompletely(buf);
|
||||
|
||||
assert.strictEqual(ex.length, 7);
|
||||
assert.deepStrictEqual(ex, new Uint8Array([1, 2, 3, 4, 5, 6, 7]));
|
||||
assert.strictEqual(buf.remains(), 0);
|
||||
});
|
||||
|
||||
it("Reader", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), (data) => {
|
||||
return data;
|
||||
}),
|
||||
expected = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7,
|
||||
],
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(expected.slice(0, 8)));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = [];
|
||||
|
||||
while (result.length < expected.length) {
|
||||
result.push((await r.export(1))[0]);
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(result, expected);
|
||||
});
|
||||
|
||||
it("readOne", async () => {
|
||||
let r = new reader.Reader(new reader.Multiple(() => {}), (data) => {
|
||||
return data;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7]));
|
||||
}, 100);
|
||||
|
||||
let rr = await reader.readOne(r);
|
||||
|
||||
assert.deepStrictEqual(rr, Uint8Array.from([0]));
|
||||
|
||||
rr = await reader.readOne(r);
|
||||
|
||||
assert.deepStrictEqual(rr, Uint8Array.from([1]));
|
||||
});
|
||||
|
||||
it("readN", async () => {
|
||||
let r = new reader.Reader(new reader.Multiple(() => {}), (data) => {
|
||||
return data;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7]));
|
||||
}, 100);
|
||||
|
||||
let rr = await reader.readN(r, 3);
|
||||
|
||||
assert.deepStrictEqual(rr, Uint8Array.from([0, 1, 2]));
|
||||
|
||||
rr = await reader.readN(r, 3);
|
||||
|
||||
assert.deepStrictEqual(rr, Uint8Array.from([3, 4, 5]));
|
||||
});
|
||||
|
||||
it("Limited", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), (data) => {
|
||||
return data;
|
||||
}),
|
||||
expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
|
||||
limited = new reader.Limited(r, 10),
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(expected.slice(0, 8)));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = [];
|
||||
|
||||
while (!limited.completed()) {
|
||||
result.push((await limited.export(1))[0]);
|
||||
}
|
||||
|
||||
assert.strictEqual(limited.completed(), true);
|
||||
assert.deepStrictEqual(result, expected);
|
||||
});
|
||||
|
||||
it("readCompletely", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), (data) => {
|
||||
return data;
|
||||
}),
|
||||
expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
|
||||
limited = new reader.Limited(r, 10),
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(expected.slice(0, 8)));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = await reader.readCompletely(limited);
|
||||
|
||||
assert.strictEqual(limited.completed(), true);
|
||||
assert.deepStrictEqual(result, Uint8Array.from(expected));
|
||||
});
|
||||
|
||||
it("readUntil", async () => {
|
||||
const maxTests = 3;
|
||||
let IntvCount = 0,
|
||||
r = new reader.Reader(new reader.Multiple(() => {}), (data) => {
|
||||
return data;
|
||||
}),
|
||||
sample = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1],
|
||||
expected1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
|
||||
expected2 = new Uint8Array([0, 1]),
|
||||
limited = new reader.Limited(r, 10),
|
||||
feedIntv = setInterval(() => {
|
||||
r.feed(Uint8Array.from(sample));
|
||||
|
||||
IntvCount++;
|
||||
|
||||
if (IntvCount < maxTests) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(feedIntv);
|
||||
}, 300);
|
||||
|
||||
let result = await reader.readUntil(limited, 7);
|
||||
|
||||
assert.strictEqual(limited.completed(), false);
|
||||
assert.deepStrictEqual(result.data, expected1);
|
||||
assert.deepStrictEqual(result.found, true);
|
||||
|
||||
result = await reader.readUntil(limited, 7);
|
||||
|
||||
assert.strictEqual(limited.completed(), true);
|
||||
assert.deepStrictEqual(result.data, expected2);
|
||||
assert.deepStrictEqual(result.found, false);
|
||||
});
|
||||
});
|
||||
225
stream/sender.js
225
stream/sender.js
@ -1,225 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
import * as subscribe from "./subscribe.js";
|
||||
|
||||
export class Sender {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {function} sender Underlaying sender
|
||||
* @param {integer} maxSegSize The size of max data segment
|
||||
* @param {integer} bufferFlushDelay Buffer flush delay
|
||||
* @param {integer} maxBufferedRequests Buffer flush delay
|
||||
*
|
||||
*/
|
||||
constructor(sender, maxSegSize, bufferFlushDelay, maxBufferedRequests) {
|
||||
this.sender = sender;
|
||||
this.maxSegSize = maxSegSize;
|
||||
this.subscribe = new subscribe.Subscribe();
|
||||
this.sendingPoc = this.sending();
|
||||
this.sendDelay = null;
|
||||
this.bufferFlushDelay = bufferFlushDelay;
|
||||
this.maxBufferedRequests = maxBufferedRequests;
|
||||
this.buffer = new Uint8Array(maxSegSize);
|
||||
this.bufferUsed = 0;
|
||||
this.bufferReq = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the send delay of current sender
|
||||
*
|
||||
* @param {integer} newDelay the new delay
|
||||
*
|
||||
*/
|
||||
setDelay(newDelay) {
|
||||
this.bufferFlushDelay = newDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to the this.sender
|
||||
*
|
||||
* @param {Uint8Array} data to send
|
||||
* @param {Array<function>} callbacks to call to return send result
|
||||
*
|
||||
*/
|
||||
async sendData(data, callbacks) {
|
||||
try {
|
||||
await this.sender(data);
|
||||
|
||||
for (let i in callbacks) {
|
||||
callbacks[i].resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
for (let i in callbacks) {
|
||||
callbacks[i].reject(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append data to the end of internal buffer
|
||||
*
|
||||
* @param {Uint8Array} data data to add
|
||||
*
|
||||
* @returns {integer} How many bytes of data is added
|
||||
*
|
||||
*/
|
||||
appendBuffer(data) {
|
||||
const remainSize = this.buffer.length - this.bufferUsed,
|
||||
appendLength = data.length > remainSize ? remainSize : data.length;
|
||||
|
||||
this.buffer.set(data.slice(0, appendLength), this.bufferUsed);
|
||||
this.bufferUsed += appendLength;
|
||||
|
||||
return appendLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export current buffer and reset it to empty
|
||||
*
|
||||
* @returns {Uint8Array} Exported buffer
|
||||
*
|
||||
*/
|
||||
exportBuffer() {
|
||||
const buffer = this.buffer.slice(0, this.bufferUsed);
|
||||
|
||||
this.bufferUsed = 0;
|
||||
this.bufferedRequests = 0;
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sender proc
|
||||
*
|
||||
*/
|
||||
async sending() {
|
||||
let callbacks = [];
|
||||
|
||||
for (;;) {
|
||||
const fetched = await this.subscribe.subscribe();
|
||||
|
||||
// Force flush?
|
||||
if (fetched === true) {
|
||||
if (this.bufferUsed <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.sendData(this.exportBuffer(), callbacks);
|
||||
callbacks = [];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
callbacks.push({
|
||||
resolve: fetched.resolve,
|
||||
reject: fetched.reject,
|
||||
});
|
||||
|
||||
// Add data to buffer and maybe flush when the buffer is full
|
||||
let currentSendDataLen = 0;
|
||||
|
||||
while (fetched.data.length > currentSendDataLen) {
|
||||
const sentLen = this.appendBuffer(
|
||||
fetched.data.slice(currentSendDataLen, fetched.data.length),
|
||||
);
|
||||
|
||||
// Buffer not full, wait for the force flush
|
||||
if (this.buffer.length > this.bufferUsed) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentSendDataLen += sentLen;
|
||||
|
||||
await this.sendData(this.exportBuffer(), callbacks);
|
||||
callbacks = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear everything
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (this.sendDelay !== null) {
|
||||
clearTimeout(this.sendDelay);
|
||||
this.sendDelay = null;
|
||||
}
|
||||
|
||||
this.buffered = null;
|
||||
this.bufferUsed = 0;
|
||||
this.bufferedRequests = 0;
|
||||
|
||||
this.subscribe.reject(new Exception("Sender has been cleared", false));
|
||||
this.subscribe.disable();
|
||||
|
||||
this.sendingPoc.catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data
|
||||
*
|
||||
* @param {Uint8Array} data data to send
|
||||
*
|
||||
* @throws {Exception} when sending has been cancelled
|
||||
*
|
||||
* @returns {Promise} will be resolved when the data is send and will be
|
||||
* rejected when the data is not
|
||||
*
|
||||
*/
|
||||
send(data) {
|
||||
let delayCleared = false;
|
||||
|
||||
if (this.sendDelay !== null) {
|
||||
clearTimeout(this.sendDelay);
|
||||
this.sendDelay = null;
|
||||
delayCleared = true;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
self.subscribe.resolve({
|
||||
data: data,
|
||||
resolve: resolve,
|
||||
reject: reject,
|
||||
});
|
||||
|
||||
if (self.bufferedRequests >= self.maxBufferedRequests) {
|
||||
self.bufferedRequests = 0;
|
||||
|
||||
self.subscribe.resolve(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (delayCleared) {
|
||||
self.bufferedRequests++;
|
||||
}
|
||||
|
||||
self.sendDelay = setTimeout(() => {
|
||||
self.sendDelay = null;
|
||||
self.bufferedRequests = 0;
|
||||
|
||||
self.subscribe.resolve(true);
|
||||
}, self.bufferFlushDelay);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import assert from "assert";
|
||||
import * as sender from "./sender.js";
|
||||
|
||||
describe("Sender", () => {
|
||||
function generateTestData(size) {
|
||||
let d = new Uint8Array(size);
|
||||
|
||||
for (let i = 0; i < d.length; i++) {
|
||||
d[i] = i % 256;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
it("Send", async () => {
|
||||
const maxSegSize = 64;
|
||||
let result = [];
|
||||
let sd = new sender.Sender(
|
||||
(rawData) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
for (let i in rawData) {
|
||||
result.push(rawData[i]);
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, 5);
|
||||
});
|
||||
},
|
||||
maxSegSize,
|
||||
300,
|
||||
3,
|
||||
);
|
||||
let expected = generateTestData(maxSegSize * 16);
|
||||
|
||||
sd.send(expected);
|
||||
|
||||
let sendCompleted = new Promise((resolve) => {
|
||||
let timer = setInterval(() => {
|
||||
if (result.length < expected.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
await sendCompleted;
|
||||
|
||||
assert.deepStrictEqual(new Uint8Array(result), expected);
|
||||
});
|
||||
|
||||
it("Send (Multiple calls)", async () => {
|
||||
const maxSegSize = 64;
|
||||
let result = [];
|
||||
let sd = new sender.Sender(
|
||||
(rawData) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
for (let i in rawData) {
|
||||
result.push(rawData[i]);
|
||||
}
|
||||
|
||||
resolve();
|
||||
}, 10);
|
||||
});
|
||||
},
|
||||
maxSegSize,
|
||||
300,
|
||||
100,
|
||||
);
|
||||
let expectedSingle = generateTestData(maxSegSize * 2),
|
||||
expectedLen = expectedSingle.length * 16,
|
||||
expected = new Uint8Array(expectedLen);
|
||||
|
||||
for (let i = 0; i < expectedLen; i += expectedSingle.length) {
|
||||
expected.set(expectedSingle, i);
|
||||
}
|
||||
|
||||
for (let i = 0; i < expectedLen; i += expectedSingle.length) {
|
||||
setTimeout(() => {
|
||||
sd.send(expectedSingle);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
let sendCompleted = new Promise((resolve) => {
|
||||
let timer = setInterval(() => {
|
||||
if (result.length < expectedLen) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
await sendCompleted;
|
||||
|
||||
assert.deepStrictEqual(new Uint8Array(result), expected);
|
||||
});
|
||||
});
|
||||
363
stream/stream.js
363
stream/stream.js
@ -1,363 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as common from "./common.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as header from "./header.js";
|
||||
import * as reader from "./reader.js";
|
||||
import * as sender from "./sender.js";
|
||||
|
||||
export class Sender {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} id ID of the stream
|
||||
* @param {sender.Sender} sd The data sender
|
||||
*
|
||||
*/
|
||||
constructor(id, sd) {
|
||||
this.id = id;
|
||||
this.sender = sd;
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to remote
|
||||
*
|
||||
* @param {number} marker binary marker
|
||||
* @param {Uint8Array} data data to be sent
|
||||
*
|
||||
* @throws {Exception} When the sender already been closed
|
||||
*
|
||||
*/
|
||||
send(marker, data) {
|
||||
if (this.closed) {
|
||||
throw new Exception(
|
||||
"Sender already been closed. No data can be send",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let reqHeader = new header.Header(header.STREAM),
|
||||
stHeader = new header.Stream(0, 0),
|
||||
d = new Uint8Array(data.length + 3);
|
||||
|
||||
reqHeader.set(this.id);
|
||||
stHeader.set(marker, data.length);
|
||||
|
||||
d[0] = reqHeader.value();
|
||||
d.set(stHeader.buffer(), 1);
|
||||
d.set(data, 3);
|
||||
|
||||
return this.sender.send(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to remote, if the data is too long, it will be separated into
|
||||
* different stream requests
|
||||
*
|
||||
* @param {number} marker binary marker
|
||||
* @param {Uint8Array} data data to be sent
|
||||
*
|
||||
* @throws {Exception} When the sender already been closed
|
||||
*
|
||||
*/
|
||||
async sendData(marker, data) {
|
||||
if (this.closed) {
|
||||
throw new Exception(
|
||||
"Sender already been closed. No data can be send",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let dataSeg = common.separateBuffer(data, header.STREAM_MAX_LENGTH),
|
||||
reqHeader = new header.Header(header.STREAM);
|
||||
|
||||
reqHeader.set(this.id);
|
||||
|
||||
for (let i in dataSeg) {
|
||||
let stHeader = new header.Stream(0, 0),
|
||||
d = new Uint8Array(dataSeg[i].length + 3);
|
||||
|
||||
stHeader.set(marker, dataSeg[i].length);
|
||||
|
||||
d[0] = reqHeader.value();
|
||||
d.set(stHeader.buffer(), 1);
|
||||
d.set(dataSeg[i], 3);
|
||||
|
||||
await this.sender.send(d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send stream signals
|
||||
*
|
||||
* @param {number} signal Signal value
|
||||
*
|
||||
* @throws {Exception} When the sender already been closed
|
||||
*
|
||||
*/
|
||||
signal(signal) {
|
||||
if (this.closed) {
|
||||
throw new Exception(
|
||||
"Sender already been closed. No signal can be send",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let reqHeader = new header.Header(signal);
|
||||
|
||||
reqHeader.set(this.id);
|
||||
|
||||
return this.sender.send(new Uint8Array([reqHeader.value()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send close signal and close current sender
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let r = this.signal(header.CLOSE);
|
||||
|
||||
this.closed = true;
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
export class InitialSender {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} id ID of the stream
|
||||
* @param {number} commandID ID of the command
|
||||
* @param {sender.Sender} sd The data sender
|
||||
*
|
||||
*/
|
||||
constructor(id, commandID, sd) {
|
||||
this.id = id;
|
||||
this.command = commandID;
|
||||
this.sender = sd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return how large the data can be
|
||||
*
|
||||
* @returns {number} Max data size
|
||||
*
|
||||
*/
|
||||
static maxDataLength() {
|
||||
return header.InitialStream.maxDataSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to remote
|
||||
*
|
||||
* @param {Uint8Array} data data to be sent
|
||||
*
|
||||
*/
|
||||
send(data) {
|
||||
let reqHeader = new header.Header(header.STREAM),
|
||||
stHeader = new header.InitialStream(0, 0),
|
||||
d = new Uint8Array(data.length + 3);
|
||||
|
||||
reqHeader.set(this.id);
|
||||
stHeader.set(this.command, data.length, true);
|
||||
|
||||
d[0] = reqHeader.value();
|
||||
d.set(stHeader.buffer(), 1);
|
||||
d.set(data, 3);
|
||||
|
||||
return this.sender.send(d);
|
||||
}
|
||||
}
|
||||
|
||||
export class Stream {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {number} id ID of the stream
|
||||
*
|
||||
*/
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.command = null;
|
||||
this.isInitializing = false;
|
||||
this.isShuttingDown = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not current stream is running
|
||||
*
|
||||
* @returns {boolean} True when it's running, false otherwise
|
||||
*
|
||||
*/
|
||||
running() {
|
||||
return this.command !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not current stream is initializing
|
||||
*
|
||||
* @returns {boolean} True when it's initializing, false otherwise
|
||||
*
|
||||
*/
|
||||
initializing() {
|
||||
return this.isInitializing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets current stream
|
||||
*
|
||||
*/
|
||||
clear() {
|
||||
this.command = null;
|
||||
this.isInitializing = false;
|
||||
this.isShuttingDown = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the stream for a new command
|
||||
*
|
||||
* @param {number} commandID Command ID
|
||||
* @param {function} commandBuilder Function that returns a command
|
||||
* @param {sender.Sender} sd Data sender
|
||||
*
|
||||
* @throws {Exception} when stream already running
|
||||
*
|
||||
*/
|
||||
run(commandID, commandBuilder, sd) {
|
||||
if (this.running()) {
|
||||
throw new Exception(
|
||||
"Stream already running, cannot accept new commands",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
this.isInitializing = true;
|
||||
this.command = commandBuilder(new Sender(this.id, sd));
|
||||
|
||||
return this.command.run(new InitialSender(this.id, commandID, sd));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when initialization respond has been received
|
||||
*
|
||||
* @param {header.InitialStream} streamInitialHeader Stream Initial header
|
||||
*
|
||||
* @throws {Exception} When the stream is not running, or been shutting down
|
||||
*
|
||||
*/
|
||||
initialize(hd) {
|
||||
if (!this.running()) {
|
||||
throw new Exception(
|
||||
"Cannot initialize a stream that is not running",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Cannot initialize a stream that is about to shutdown",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
this.command.initialize(hd);
|
||||
|
||||
if (!hd.success()) {
|
||||
this.clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isInitializing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Stream data has been received
|
||||
*
|
||||
* @param {header.Stream} streamHeader Stream header
|
||||
* @param {reader.Limited} rd Data reader
|
||||
*
|
||||
* @throws {Exception} When the stream is not running, or shutting down
|
||||
*
|
||||
*/
|
||||
tick(streamHeader, rd) {
|
||||
if (!this.running()) {
|
||||
throw new Exception("Cannot tick a stream that is not running", false);
|
||||
}
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Cannot tick a stream that is about to shutdown",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return this.command.tick(streamHeader, rd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when stream close request has been received
|
||||
*
|
||||
* @throws {Exception} When the stream is not running, or shutting down
|
||||
*
|
||||
*/
|
||||
close() {
|
||||
if (!this.running()) {
|
||||
throw new Exception("Cannot close a stream that is not running", false);
|
||||
}
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Cannot close a stream that is about to shutdown",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
this.isShuttingDown = true;
|
||||
this.command.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when stream completed respond has been received
|
||||
*
|
||||
* @throws {Exception} When stream isn't running, or not shutting down
|
||||
*
|
||||
*/
|
||||
completed() {
|
||||
if (!this.running()) {
|
||||
throw new Exception("Cannot close a stream that is not running", false);
|
||||
}
|
||||
|
||||
if (!this.isShuttingDown) {
|
||||
throw new Exception(
|
||||
"Can't complete current stream because Close " +
|
||||
"signal is not received",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
this.command.completed();
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
@ -1,436 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as common from "./common.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as header from "./header.js";
|
||||
import * as reader from "./reader.js";
|
||||
import * as sender from "./sender.js";
|
||||
import * as stream from "./stream.js";
|
||||
|
||||
export const ECHO_FAILED = -1;
|
||||
|
||||
export class Requested {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {stream.Stream} stream The selected stream
|
||||
* @param {any} result Result of the run
|
||||
*
|
||||
*/
|
||||
constructor(stream, result) {
|
||||
this.stream = stream;
|
||||
this.result = result;
|
||||
}
|
||||
}
|
||||
|
||||
export class Streams {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {reader.Reader} reader The data reader
|
||||
* @param {sender.Sender} sender The data sender
|
||||
* @param {object} config Configuration
|
||||
*/
|
||||
constructor(reader, sender, config) {
|
||||
this.reader = reader;
|
||||
this.sender = sender;
|
||||
this.config = config;
|
||||
this.echoTimer = null;
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
this.stop = false;
|
||||
|
||||
this.streams = [];
|
||||
|
||||
for (let i = 0; i <= header.HEADER_MAX_DATA; i++) {
|
||||
this.streams.push(new stream.Stream(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts stream proccessing
|
||||
*
|
||||
* @returns {Promise<true>} When service is completed
|
||||
*
|
||||
* @throws {Exception} When the process already started
|
||||
*
|
||||
*/
|
||||
async serve() {
|
||||
if (this.echoTimer !== null) {
|
||||
throw new Exception("Already started", false);
|
||||
}
|
||||
|
||||
this.echoTimer = setInterval(() => {
|
||||
this.sendEcho();
|
||||
}, this.config.echoInterval);
|
||||
|
||||
this.stop = false;
|
||||
|
||||
this.sendEcho();
|
||||
|
||||
let ee = null;
|
||||
|
||||
while (!this.stop && ee === null) {
|
||||
try {
|
||||
await this.tick();
|
||||
} catch (e) {
|
||||
if (!e.temporary) {
|
||||
ee = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.clear(ee);
|
||||
|
||||
if (ee !== null) {
|
||||
throw new Exception("Streams is closed: " + ee, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear current proccess
|
||||
*
|
||||
* @param {Exception} e An error caused this clear. Null when no error
|
||||
*
|
||||
*/
|
||||
clear(e) {
|
||||
if (this.stop) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop = true;
|
||||
|
||||
if (this.echoTimer != null) {
|
||||
clearInterval(this.echoTimer);
|
||||
this.echoTimer = null;
|
||||
}
|
||||
|
||||
for (let i in this.streams) {
|
||||
if (!this.streams[i].running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
this.streams[i].close();
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
try {
|
||||
this.streams[i].completed();
|
||||
} catch (e) {
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this.sender.close();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.reader.close();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
|
||||
this.config.cleared(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request remote to pause stream sending
|
||||
*
|
||||
*/
|
||||
pause() {
|
||||
let pauseHeader = header.header(header.CONTROL);
|
||||
|
||||
pauseHeader.set(1);
|
||||
|
||||
return this.sender.send(
|
||||
new Uint8Array([pauseHeader.value(), header.CONTROL_PAUSESTREAM]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request remote to resume stream sending
|
||||
*
|
||||
*/
|
||||
resume() {
|
||||
let pauseHeader = header.header(header.CONTROL);
|
||||
|
||||
pauseHeader.set(1);
|
||||
|
||||
return this.sender.send(
|
||||
new Uint8Array([pauseHeader.value(), header.CONTROL_RESUMESTREAM]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request stream for given command
|
||||
*
|
||||
* @param {number} commandID Command ID
|
||||
* @param {function} commandBuilder Command builder
|
||||
*
|
||||
* @returns {Requested} The result of the stream command
|
||||
*
|
||||
*/
|
||||
request(commandID, commandBuilder) {
|
||||
try {
|
||||
for (let i in this.streams) {
|
||||
if (this.streams[i].running()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Requested(
|
||||
this.streams[i],
|
||||
this.streams[i].run(commandID, commandBuilder, this.sender),
|
||||
);
|
||||
}
|
||||
|
||||
throw new Exception("No stream is currently available", true);
|
||||
} catch (e) {
|
||||
throw new Exception("Stream request has failed: " + e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send echo request
|
||||
*
|
||||
*/
|
||||
sendEcho() {
|
||||
let echoHeader = header.header(header.CONTROL),
|
||||
randomNum = new Uint8Array(common.getRands(8, 0, 255));
|
||||
|
||||
echoHeader.set(randomNum.length - 1);
|
||||
|
||||
randomNum[0] = echoHeader.value();
|
||||
randomNum[1] = header.CONTROL_ECHO;
|
||||
|
||||
this.sender.send(randomNum).then(() => {
|
||||
if (this.lastEchoTime !== null || this.lastEchoData !== null) {
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
|
||||
this.config.echoUpdater(ECHO_FAILED);
|
||||
}
|
||||
|
||||
this.lastEchoTime = new Date();
|
||||
this.lastEchoData = randomNum.slice(2, randomNum.length);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received control request
|
||||
*
|
||||
* @param {reader.Reader} rd The reader
|
||||
*
|
||||
*/
|
||||
async handleControl(rd) {
|
||||
let controlType = await reader.readOne(rd),
|
||||
delay = 0,
|
||||
echoBytes = null;
|
||||
|
||||
switch (controlType[0]) {
|
||||
case header.CONTROL_ECHO:
|
||||
echoBytes = await reader.readCompletely(rd);
|
||||
|
||||
if (this.lastEchoTime === null || this.lastEchoData === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.lastEchoData.length !== echoBytes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i in this.lastEchoData) {
|
||||
if (this.lastEchoData[i] == echoBytes[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
|
||||
this.config.echoUpdater(ECHO_FAILED);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
delay = new Date().getTime() - this.lastEchoTime.getTime();
|
||||
|
||||
if (delay < 0) {
|
||||
delay = 0;
|
||||
}
|
||||
|
||||
this.lastEchoTime = null;
|
||||
this.lastEchoData = null;
|
||||
|
||||
this.config.echoUpdater(delay);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await reader.readCompletely(rd);
|
||||
|
||||
throw new Exception("Unknown control signal: " + controlType);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received stream respond
|
||||
*
|
||||
* @param {header.Header} hd The header
|
||||
* @param {reader.Reader} rd The reader
|
||||
*
|
||||
* @throws {Exception} when given stream is not running
|
||||
*
|
||||
*/
|
||||
async handleStream(hd, rd) {
|
||||
if (hd.data() >= this.streams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = this.streams[hd.data()];
|
||||
|
||||
if (!stream.running()) {
|
||||
// WARNING: Connection must be reset at this point because we cannot
|
||||
// determine how many bytes to read
|
||||
throw new Exception(
|
||||
'Remote is requesting for stream "' +
|
||||
hd.data() +
|
||||
'" which is not running',
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let initialHeaderBytes = await reader.readN(rd, 2);
|
||||
|
||||
// WARNING: It's the stream's responsibility to ensure stream data is
|
||||
// completely readed before return
|
||||
if (stream.initializing()) {
|
||||
let streamHeader = new header.InitialStream(
|
||||
initialHeaderBytes[0],
|
||||
initialHeaderBytes[1],
|
||||
);
|
||||
|
||||
return stream.initialize(streamHeader);
|
||||
}
|
||||
|
||||
let streamHeader = new header.Stream(
|
||||
initialHeaderBytes[0],
|
||||
initialHeaderBytes[1],
|
||||
),
|
||||
streamReader = new reader.Limited(rd, streamHeader.length());
|
||||
|
||||
let tickResult = await stream.tick(streamHeader, streamReader);
|
||||
|
||||
await reader.readCompletely(streamReader);
|
||||
|
||||
return tickResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received close respond
|
||||
*
|
||||
* @param {header.Header} hd The header
|
||||
*
|
||||
* @throws {Exception} when given stream is not running
|
||||
*
|
||||
*/
|
||||
async handleClose(hd) {
|
||||
if (hd.data() >= this.streams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = this.streams[hd.data()];
|
||||
|
||||
if (!stream.running()) {
|
||||
// WARNING: Connection must be reset at this point because we cannot
|
||||
// determine how many bytes to read
|
||||
throw new Exception(
|
||||
'Remote is requesting for stream "' +
|
||||
hd.data() +
|
||||
'" to be closed, but the stream is not running',
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let cResult = await stream.close();
|
||||
|
||||
let completedHeader = new header.Header(header.COMPLETED);
|
||||
completedHeader.set(hd.data());
|
||||
this.sender.send(new Uint8Array([completedHeader.value()]));
|
||||
|
||||
return cResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* handle received close respond
|
||||
*
|
||||
* @param {header.Header} hd The header
|
||||
*
|
||||
* @throws {Exception} when given stream is not running
|
||||
*
|
||||
*/
|
||||
async handleCompleted(hd) {
|
||||
if (hd.data() >= this.streams.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = this.streams[hd.data()];
|
||||
|
||||
if (!stream.running()) {
|
||||
// WARNING: Connection must be reset at this point because we cannot
|
||||
// determine how many bytes to read
|
||||
throw new Exception(
|
||||
'Remote is requesting for stream "' +
|
||||
hd.data() +
|
||||
'" to be completed, but the stream is not running',
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return stream.completed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main proccess loop
|
||||
*
|
||||
* @throws {Exception} when encountered an unknown header
|
||||
*/
|
||||
async tick() {
|
||||
let headerBytes = await reader.readOne(this.reader),
|
||||
hd = new header.Header(headerBytes[0]);
|
||||
|
||||
switch (hd.type()) {
|
||||
case header.CONTROL:
|
||||
return this.handleControl(new reader.Limited(this.reader, hd.data()));
|
||||
|
||||
case header.STREAM:
|
||||
return this.handleStream(hd, this.reader);
|
||||
|
||||
case header.CLOSE:
|
||||
return this.handleClose(hd);
|
||||
|
||||
case header.COMPLETED:
|
||||
return this.handleCompleted(hd);
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown header", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
describe("Streams", () => {
|
||||
it("Header", () => {});
|
||||
});
|
||||
@ -1,129 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
|
||||
const typeReject = 0;
|
||||
const typeResolve = 1;
|
||||
|
||||
export class Subscribe {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
this.res = null;
|
||||
this.rej = null;
|
||||
this.pending = [];
|
||||
this.disabled = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many resolve/reject in the pending
|
||||
*/
|
||||
pendings() {
|
||||
return (
|
||||
this.pending.length + (this.rej !== null || this.res !== null ? 1 : 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the subscribe waiter
|
||||
*
|
||||
* @param {any} d Resolve data which will be send to the subscriber
|
||||
*/
|
||||
resolve(d) {
|
||||
if (this.res !== null) {
|
||||
this.res(d);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.pending.push([typeResolve, d]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject the subscribe waiter
|
||||
*
|
||||
* @param {any} e Error message that will be send to the subscriber
|
||||
*
|
||||
*/
|
||||
reject(e) {
|
||||
if (this.rej !== null) {
|
||||
this.rej(e);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.pending.push([typeReject, e]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waiting and receive subscribe data
|
||||
*
|
||||
* @returns {Promise<any>} Data receiver
|
||||
*
|
||||
*/
|
||||
subscribe() {
|
||||
if (this.pending.length > 0) {
|
||||
let p = this.pending.shift();
|
||||
|
||||
switch (p[0]) {
|
||||
case typeReject:
|
||||
throw p[1];
|
||||
|
||||
case typeResolve:
|
||||
return p[1];
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown pending type", false);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.disabled) {
|
||||
throw new Exception(this.disabled, false);
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
self.res = (d) => {
|
||||
self.res = null;
|
||||
self.rej = null;
|
||||
|
||||
resolve(d);
|
||||
};
|
||||
|
||||
self.rej = (e) => {
|
||||
self.res = null;
|
||||
self.rej = null;
|
||||
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable current subscriber when all internal data is readed
|
||||
*
|
||||
* @param {string} reason Reason of the disable
|
||||
*
|
||||
*/
|
||||
disable(reason) {
|
||||
this.disabled = reason;
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="150" width="150">
|
||||
<path d="M29.49 50.27a.912.912 0 0 0-.13.012 1.125 1.141 0 0 0-.364.117l-23 12.035a1.097 1.112 0 0 0-.589 1.005v2.958a.314.319 0 0 0 0 .012 1.092 1.108 0 0 0 .577.97l20.897 11.708a.412.418 0 0 1 .212.372V91a1.118 1.118 0 0 0 1.111 1.11h1.311a1.105 1.12 0 0 0 1.086-1.11V78.163c0-.258.168-.416.424-.416h12.406c.255 0 .415.16.415.416v12.838a1.119 1.119 0 0 0 1.111 1.11h1.308a1.108 1.124 0 0 0 1.085-1.11V68c0-.175.038-.247.084-.298a.459.465 0 0 1 .248-.121.471.478 0 0 1 .283.012c.056.025.113.077.178.205.006.013.006.006.012.019a.314.319 0 0 0 0 .012l9.574 21.25c.195.431.613.667 1.015.66a1.105 1.12 0 0 0 .992-.694l6.031-15.204c.095-.24.203-.263.403-.263s.295.024.39.26l6.022 15.208c.17.451.581.708.99.717a1.116 1.132 0 0 0 1.039-.67l9.56-21.264c.073-.16.131-.201.191-.228a.437.443 0 0 1 .27-.013.459.465 0 0 1 .249.122c.047.049.081.122.083.298v22.988a.314.319 0 0 0 0 .012 1.105 1.12 0 0 0 1.11 1.1h1.312a1.094 1.11 0 0 0 1.095-1.1.314.319 0 0 0 0-.012V65.11a1.094 1.11 0 0 0-1.095-1.1h-4.512a1.092 1.108 0 0 0-1.005.633.314.319 0 0 0-.01 0c-2.652 5.835-5.294 11.682-7.945 17.515-.102.225-.207.257-.402.252-.194-.004-.286-.049-.377-.276l-5.95-14.867a1.1 1.116 0 0 0-1.016-.717 1.116 1.132 0 0 0-1.027.717L59.18 82.134c-.094.232-.186.271-.377.276-.197.005-.29-.024-.392-.252L50.47 64.643a1.108 1.124 0 0 0-1.004-.634h-4.51a1.103 1.119 0 0 0-1.11 1.1v8.622c0 .257-.169.443-.415.443H31.025c-.248 0-.424-.186-.424-.444v-8.62a1.097 1.112 0 0 0-1.086-1.1h-1.311a1.103 1.119 0 0 0-1.111 1.1v9.149a.752.752 0 0 1-.27.526c-.18.133-.205.138-.332.071a.314.319 0 0 0-.013 0 .314.319 0 0 0-.047-.024.314.319 0 0 0-.036-.012c-5.772-3.016-11.524-6.302-17.304-9.386a.434.44 0 0 1-.212-.265 1.43 1.45 0 0 1 .012-.441.959.972 0 0 1 .223-.324 1.09 1.106 0 0 1 .164-.12c.077-.024.157-.054.226-.095 6.808-3.626 13.682-7.116 20.504-10.694a1.103 1.119 0 0 0 .593-.99V51.39a1.105 1.12 0 0 0-1.111-1.125zm63.89 13.744a1.108 1.124 0 0 0-1.096 1.126v25.857a1.116 1.132 0 0 0 1.108 1.116h1.298a1.106 1.122 0 0 0 1.1-1.116V78.163c0-.255.172-.417.426-.417h10.823a1.103 1.119 0 0 0 1.099-1.126v-1.342a1.097 1.112 0 0 0-1.1-1.1H96.216c-.247 0-.426-.188-.426-.444V68c0-.255.172-.42.426-.42H115.6c.254 0 .414.163.414.42v22.997c.005.606.488 1.108 1.085 1.116a.314.319 0 0 0 .01 0h1.299a1.114 1.13 0 0 0 1.11-1.116V68c0-.259.161-.42.416-.42h7.14a.41.41 0 0 1 .355.192l7.615 12.249a.393.398 0 0 1 .07.215v10.76a1.111 1.127 0 0 0 1.087 1.116h1.307a1.111 1.127 0 0 0 1.088-1.115V80.236c0-.07.022-.137.07-.215l8.867-14.296c.454-.727-.072-1.708-.92-1.711h-1.501a1.1 1.116 0 0 0-.943.516l-6.967 11.205c-.1.161-.228.215-.354.215-.125 0-.251-.055-.352-.215L129.52 64.53a1.094 1.11 0 0 0-.921-.516c-11.732 0-23.473-.007-35.206 0zM3.411 70.706a.631.631 0 0 0-.107.012 1.094 1.11 0 0 0-1.005 1.1v1.294a1.12 1.136 0 0 0 .582.983l16.998 9.481.012.012a.314.319 0 0 0 .037.025.314.319 0 0 0 .047.037c.146.08.223.202.223.36v1.96c0 .16-.074.297-.212.375a.314.319 0 0 0-.071.049L2.869 96.307a1.1 1.116 0 0 0-.569.969v1.354a.314.319 0 0 0 0 .013c.007.841.939 1.367 1.655.943l18.699-10.98a1.099 1.114 0 0 0 .543-.972v-5.207a1.105 1.12 0 0 0-.567-.972C16.4 77.922 3.944 70.85 3.944 70.85a.314.319 0 0 0-.014 0 1.083 1.098 0 0 0-.518-.143z" fill="none" stroke="#e9a" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="20 30 10 40" stroke-dashoffset="-100">
|
||||
<animate restart="always" repeatCount="indefinite" dur="3s" to="100" from="-100" attributeName="stroke-dashoffset" begin="0s"/>
|
||||
<animate id="l1" dur="5s" to="40 10 50 0" from="20 30 10 40" attributeName="stroke-dasharray" begin="0s;l2.end" calcMode="paced"/>
|
||||
<animate id="l2" dur="5s" to="20 30 10 40" from="40 10 50 0" attributeName="stroke-dasharray" begin="l1.end" calcMode="paced"/>
|
||||
</path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB |
@ -1,316 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<slot />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint vue/attribute-hyphenation: 0 */
|
||||
|
||||
const XMLNG = "http://www.w3.org/2000/svg";
|
||||
const XMLNS = "http://www.w3.org/2000/xmlns/";
|
||||
const XMLNGLink = "http://www.w3.org/1999/xlink";
|
||||
|
||||
class Data {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.max = this.getMax(data);
|
||||
}
|
||||
|
||||
setMax(max) {
|
||||
this.max = this.max > max ? this.max : max;
|
||||
}
|
||||
|
||||
getMax(data) {
|
||||
let max = 0;
|
||||
|
||||
for (let i in data) {
|
||||
if (data[i].data <= max) {
|
||||
continue;
|
||||
}
|
||||
|
||||
max = data[i].data;
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
class BaseDrawer {
|
||||
constructor() {
|
||||
this.elements = [];
|
||||
}
|
||||
|
||||
toCellHeight(cellHeight, data, n) {
|
||||
if (data.max === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (cellHeight / data.max) * n;
|
||||
}
|
||||
|
||||
toBottomHeight(cellHeight, n) {
|
||||
return cellHeight - n;
|
||||
}
|
||||
|
||||
cellWidth(rootDim, data) {
|
||||
return rootDim.width / data.data.length;
|
||||
}
|
||||
|
||||
createEl(parent, tag, properties) {
|
||||
let np = document.createElementNS(XMLNG, tag);
|
||||
|
||||
for (let p in properties) {
|
||||
if (p.indexOf("xlink:") === 0) {
|
||||
np.setAttributeNS(XMLNGLink, p, properties[p]);
|
||||
} else if (p.indexOf("xmlns:") === 0) {
|
||||
np.setAttributeNS(XMLNS, p, properties[p]);
|
||||
} else {
|
||||
np.setAttribute(p, properties[p]);
|
||||
}
|
||||
}
|
||||
|
||||
parent.appendChild(np);
|
||||
|
||||
this.elements.push(np);
|
||||
|
||||
return np;
|
||||
}
|
||||
|
||||
removeAllEl(parent) {
|
||||
for (let i in this.elements) {
|
||||
parent.removeChild(this.elements[i]);
|
||||
}
|
||||
|
||||
this.elements = [];
|
||||
}
|
||||
|
||||
draw(parent, rootDim, data) {}
|
||||
}
|
||||
|
||||
class BarDrawer extends BaseDrawer {
|
||||
constructor(topBottomPadding) {
|
||||
super();
|
||||
|
||||
this.topBottomPadding = topBottomPadding;
|
||||
}
|
||||
|
||||
draw(parent, rootDim, data) {
|
||||
let cellWidth = this.cellWidth(rootDim, data),
|
||||
currentWidth = cellWidth / 2,
|
||||
cellHalfHeight = rootDim.height - this.topBottomPadding / 2,
|
||||
cellHeight = rootDim.height - this.topBottomPadding;
|
||||
|
||||
for (let i in data.data) {
|
||||
let h = this.toCellHeight(cellHeight, data, data.data[i].data);
|
||||
|
||||
this.createEl(parent, "path", {
|
||||
d:
|
||||
"M" +
|
||||
currentWidth +
|
||||
"," +
|
||||
Math.round(this.toBottomHeight(cellHalfHeight, h)) +
|
||||
" L" +
|
||||
currentWidth +
|
||||
"," +
|
||||
cellHalfHeight,
|
||||
class: h === 0 ? "zero" : data.data[i].class,
|
||||
});
|
||||
|
||||
currentWidth += cellWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UpsideDownBarDrawer extends BarDrawer {
|
||||
draw(parent, rootDim, data) {
|
||||
let cellWidth = this.cellWidth(rootDim, data),
|
||||
currentWidth = cellWidth / 2,
|
||||
padHalfHeight = this.topBottomPadding / 2,
|
||||
cellHeight = rootDim.height - this.topBottomPadding;
|
||||
|
||||
for (let i in data.data) {
|
||||
let h = this.toCellHeight(cellHeight, data, data.data[i].data);
|
||||
|
||||
this.createEl(parent, "path", {
|
||||
d:
|
||||
"M" +
|
||||
currentWidth +
|
||||
"," +
|
||||
padHalfHeight +
|
||||
" L" +
|
||||
currentWidth +
|
||||
"," +
|
||||
(Math.round(h) + padHalfHeight),
|
||||
class: h === 0 ? "zero" : data.data[i].class,
|
||||
});
|
||||
|
||||
currentWidth += cellWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Chart {
|
||||
constructor(el, width, height, drawer) {
|
||||
this.el = el;
|
||||
this.drawer = drawer;
|
||||
this.group = null;
|
||||
this.paths = [];
|
||||
this.dim = { width, height };
|
||||
|
||||
this.el.setAttribute(
|
||||
"viewBox",
|
||||
"0 0 " +
|
||||
parseInt(this.dim.width, 10) +
|
||||
" " +
|
||||
parseInt(this.dim.height, 10),
|
||||
);
|
||||
|
||||
this.el.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||
}
|
||||
|
||||
getGroupRoot() {
|
||||
if (this.group) {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
this.group = document.createElementNS(XMLNG, "g");
|
||||
|
||||
this.el.appendChild(this.group);
|
||||
|
||||
return this.group;
|
||||
}
|
||||
|
||||
draw(data, manualMax) {
|
||||
let d = new Data(data);
|
||||
let max = d.max;
|
||||
|
||||
d.setMax(manualMax);
|
||||
|
||||
this.drawer.removeAllEl(this.getGroupRoot());
|
||||
this.drawer.draw(this.getGroupRoot(), this.dim, d);
|
||||
|
||||
return {
|
||||
dataMax: max,
|
||||
resultMax: d.max,
|
||||
};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.drawer.removeAllEl();
|
||||
this.el.removeChild(this.getGroupRoot());
|
||||
}
|
||||
}
|
||||
|
||||
function buildDrawer(type) {
|
||||
switch (type) {
|
||||
case "Bar":
|
||||
return new BarDrawer(10);
|
||||
|
||||
case "UpsideDownBar":
|
||||
return new UpsideDownBarDrawer(10);
|
||||
}
|
||||
|
||||
return new Error("Undefined drawer: " + type);
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
values: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
previousMax: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
values() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
},
|
||||
max() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
},
|
||||
enabled(newVal) {
|
||||
if (!newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.chart = new Chart(
|
||||
this.$el,
|
||||
this.width,
|
||||
this.height,
|
||||
buildDrawer(this.type),
|
||||
);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.chart.clear();
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
let r = this.chart.draw(this.values, this.max);
|
||||
|
||||
if (r.dataMax === this.previousMax) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("max", r.dataMax);
|
||||
this.previousMax = r.dataMax;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,120 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect {
|
||||
z-index: 999999;
|
||||
top: 40px;
|
||||
left: 159px;
|
||||
display: none;
|
||||
background: #333;
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
#connect-frame {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connect .window-frame {
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#connect:before {
|
||||
left: 30px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
#connect {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#connect:before {
|
||||
left: 169px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#connect:before {
|
||||
left: 149px;
|
||||
}
|
||||
}
|
||||
|
||||
#connect.display {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#connect h1 {
|
||||
padding: 15px 15px 0 15px;
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#connect-close {
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
#connect-busy-overlay {
|
||||
z-index: 2;
|
||||
background: #2229 url("busy.svg") center center no-repeat;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
#connect-warning {
|
||||
padding: 20px;
|
||||
font-size: 0.85em;
|
||||
background: #b44;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-warning-icon {
|
||||
float: left;
|
||||
display: block;
|
||||
margin: 5px 20px 5px 0;
|
||||
}
|
||||
|
||||
#connect-warning-icon::after {
|
||||
background: #c55;
|
||||
}
|
||||
|
||||
#connect-warning-msg {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#connect-warning-msg p {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#connect-warning-msg a {
|
||||
color: #faa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@ -1,190 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<window
|
||||
id="connect"
|
||||
flash-class="home-window-display"
|
||||
:display="display"
|
||||
@display="$emit('display', $event)"
|
||||
>
|
||||
<div id="connect-frame">
|
||||
<h1 class="window-title">Establish connection with</h1>
|
||||
|
||||
<slot v-if="inputting"></slot>
|
||||
|
||||
<connect-switch
|
||||
v-if="!inputting"
|
||||
:knowns-length="knowns.length"
|
||||
:tab="tab"
|
||||
@switch="switchTab"
|
||||
></connect-switch>
|
||||
|
||||
<connect-new
|
||||
v-if="tab === 'new' && !inputting"
|
||||
:connectors="connectors"
|
||||
@select="selectConnector"
|
||||
></connect-new>
|
||||
|
||||
<connect-known
|
||||
v-if="tab === 'known' && !inputting"
|
||||
:presets="presets"
|
||||
:restricted-to-presets="restrictedToPresets"
|
||||
:knowns="knowns"
|
||||
:launcher-builder="knownsLauncherBuilder"
|
||||
:knowns-export="knownsExport"
|
||||
:knowns-import="knownsImport"
|
||||
@select="selectKnown"
|
||||
@select-preset="selectPreset"
|
||||
@remove="removeKnown"
|
||||
@clear-session="clearSessionKnown"
|
||||
></connect-known>
|
||||
|
||||
<div id="connect-warning">
|
||||
<span id="connect-warning-icon" class="icon icon-warning1"></span>
|
||||
<div id="connect-warning-msg">
|
||||
<p>
|
||||
<strong>An insecured service may steal your secrets.</strong>
|
||||
Always exam the safety of the service before using it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Sshwifty is a free software, you can deploy it on your own trusted
|
||||
infrastructure.
|
||||
<a href="https://github.com/nirui/sshwifty" target="_blank"
|
||||
>Learn more</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="busy" id="connect-busy-overlay"></div>
|
||||
</div>
|
||||
</window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect.css";
|
||||
|
||||
import Window from "./window.vue";
|
||||
import ConnectSwitch from "./connect_switch.vue";
|
||||
import ConnectKnown from "./connect_known.vue";
|
||||
import ConnectNew from "./connect_new.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
window: Window,
|
||||
"connect-switch": ConnectSwitch,
|
||||
"connect-known": ConnectKnown,
|
||||
"connect-new": ConnectNew,
|
||||
},
|
||||
props: {
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inputting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
presets: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
knownsLauncherBuilder: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
knownsExport: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
knownsImport: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
connectors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
busy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tab: !this.restrictedToPresets ? "new" : "known",
|
||||
canSelect: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
switchTab(to) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tab = to;
|
||||
},
|
||||
selectConnector(connector) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("connector-select", connector);
|
||||
},
|
||||
selectKnown(known) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("known-select", known);
|
||||
},
|
||||
removeKnown(uid) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("known-remove", uid);
|
||||
},
|
||||
selectPreset(preset) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("preset-select", preset);
|
||||
},
|
||||
clearSessionKnown(uid) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("known-clear-session", uid);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,245 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect-known-list {
|
||||
min-height: 200px;
|
||||
font-size: 0.75em;
|
||||
background: #3a3a3a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connect-known-list h3 {
|
||||
font-size: 1.1em;
|
||||
color: #999;
|
||||
margin: 5px 0 15px 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#connect-known-list.reloaded {
|
||||
}
|
||||
|
||||
#connect-known-list.reloaded::after {
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
animation-name: home-window-display-flash;
|
||||
animation-duration: 0.5s;
|
||||
animation-iteration-count: 1;
|
||||
box-shadow: 0 0 10px #fff;
|
||||
}
|
||||
|
||||
#connect-known-list-list,
|
||||
#connect-known-list-empty {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
#connect-known-list-empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 1.2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-import {
|
||||
margin: 15px;
|
||||
color: #aaa;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#connect-known-list-import a {
|
||||
color: #e9a;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#connect-known-list-list {
|
||||
padding: 15px 20px 20px 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-list li {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#connect-known-list-list li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .lst-wrap {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .lst-wrap:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .labels > .type {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .labels > .opt {
|
||||
display: none;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#connect-known-list-list li > .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .labels > .opt.link {
|
||||
background: #287;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .labels > .opt.del {
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .labels > .opt.clr {
|
||||
background: #b71;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list-list li:hover > .labels > .opt,
|
||||
#connect-known-list-list li:focus > .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .lst-wrap > h4 {
|
||||
font-size: 1.5em;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .lst-wrap > h4::before {
|
||||
content: ">_";
|
||||
color: #555;
|
||||
font-size: 0.8em;
|
||||
margin-right: 5px;
|
||||
font-weight: normal;
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#connect-known-list-list li > .lst-wrap > h4.highlight::before {
|
||||
color: #eee;
|
||||
background: #555;
|
||||
}
|
||||
|
||||
#connect-known-list-presets {
|
||||
margin-top: 10px;
|
||||
padding: 15px 20px 20px 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets.last-planel {
|
||||
background: #3f3f3f;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#connect-known-list-presets li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap {
|
||||
cursor: pointer;
|
||||
border-radius: 0 3px 3px 3px;
|
||||
margin: 12px 10px 10px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > .labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > .labels > .type {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
border-radius: 3px 3px 3px 0;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > h4 {
|
||||
font-size: 1.3em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#connect-known-list-presets-alert {
|
||||
font-size: 1.15em;
|
||||
color: #fff;
|
||||
background: #c73;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@ -1,352 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="connect-known-list" :class="{ reloaded: reloaded }">
|
||||
<div
|
||||
v-if="knownList.length <= 0 && presets <= 0"
|
||||
id="connect-known-list-empty"
|
||||
>
|
||||
No known remote available
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="knownList.length > 0" id="connect-known-list-list">
|
||||
<h3>Connected before</h3>
|
||||
|
||||
<ul class="hlst lstcl1">
|
||||
<li v-for="(known, kk) in knownList" :key="kk">
|
||||
<div class="labels">
|
||||
<span
|
||||
class="type"
|
||||
:style="'background-color: ' + known.data.color"
|
||||
>
|
||||
{{ known.data.type }}
|
||||
</span>
|
||||
|
||||
<a
|
||||
class="opt link"
|
||||
href="javascript:;"
|
||||
@click="launcher(known, $event)"
|
||||
>
|
||||
{{ known.copyStatus }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="!known.data.session"
|
||||
class="opt del"
|
||||
href="javascript:;"
|
||||
@click="remove(known.data.uid)"
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="opt clr"
|
||||
href="javascript:;"
|
||||
title="Clear session data"
|
||||
@click="clearSession(known.data.uid)"
|
||||
>
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="lst-wrap" @click="select(known.data)">
|
||||
<h4
|
||||
:title="known.data.title"
|
||||
:class="{ highlight: known.data.session }"
|
||||
>
|
||||
{{ known.data.title }}
|
||||
</h4>
|
||||
Last: {{ known.data.last.toLocaleString() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="presets.length > 0"
|
||||
id="connect-known-list-presets"
|
||||
:class="{
|
||||
'last-planel': knownList.length > 0,
|
||||
}"
|
||||
>
|
||||
<h3>Presets</h3>
|
||||
|
||||
<ul class="hlst lstcl2">
|
||||
<li
|
||||
v-for="(preset, pk) in presets"
|
||||
:key="pk"
|
||||
:class="{ disabled: presetDisabled(preset) }"
|
||||
>
|
||||
<div class="lst-wrap" @click="selectPreset(preset)">
|
||||
<div class="labels">
|
||||
<span
|
||||
class="type"
|
||||
:style="'background-color: ' + preset.command.color()"
|
||||
>
|
||||
{{ preset.command.name() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 :title="preset.preset.title()">
|
||||
{{ preset.preset.title() }}
|
||||
</h4>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="restrictedToPresets" id="connect-known-list-presets-alert">
|
||||
The operator has restricted the outgoing connections. You can only
|
||||
connect to remotes from the pre-defined presets.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="connect-known-list-import">
|
||||
Tip: You can
|
||||
<a href="javascript:;" @click="importHosts">import</a> and
|
||||
<a href="javascript:;" @click="exportHosts">export</a>
|
||||
known remotes from and to a file.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect_known.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
presets: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
launcherBuilder: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
knownsExport: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
knownsImport: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
knownList: [],
|
||||
reloaded: false,
|
||||
busy: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
knowns(newVal) {
|
||||
// Only play reload animation when we're adding data into the records,
|
||||
// not reducing
|
||||
const playReloaded = newVal.length > this.knownList.length;
|
||||
|
||||
this.reload(newVal);
|
||||
|
||||
if (!playReloaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
self.reloaded = true;
|
||||
setTimeout(() => {
|
||||
self.reloaded = false;
|
||||
}, 500);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.reload(this.knowns);
|
||||
},
|
||||
methods: {
|
||||
reload(knownList) {
|
||||
this.knownList = [];
|
||||
|
||||
for (let i in knownList) {
|
||||
this.knownList.unshift({
|
||||
data: knownList[i],
|
||||
copying: false,
|
||||
copyStatus: "Copy link",
|
||||
});
|
||||
}
|
||||
},
|
||||
select(known) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("select", known);
|
||||
},
|
||||
presetDisabled(preset) {
|
||||
if (!this.restrictedToPresets || preset.preset.host().length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
selectPreset(preset) {
|
||||
if (this.busy || this.presetDisabled(preset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("select-preset", preset);
|
||||
},
|
||||
async launcher(known, ev) {
|
||||
if (known.copying || this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
this.busy = true;
|
||||
known.copying = true;
|
||||
known.copyStatus = "Copying";
|
||||
|
||||
let lnk = this.launcherBuilder(known.data);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(lnk);
|
||||
|
||||
(() => {
|
||||
known.copyStatus = "Copied!";
|
||||
})();
|
||||
} catch (e) {
|
||||
(() => {
|
||||
known.copyStatus = "Failed";
|
||||
ev.target.setAttribute("href", lnk);
|
||||
})();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
known.copyStatus = "Copy link";
|
||||
known.copying = false;
|
||||
}, 2000);
|
||||
|
||||
this.busy = false;
|
||||
},
|
||||
remove(uid) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("remove", uid);
|
||||
},
|
||||
clearSession(uid) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("clear-session", uid);
|
||||
},
|
||||
exportHosts() {
|
||||
let el = null;
|
||||
|
||||
try {
|
||||
const dataStr = JSON.stringify(this.knownsExport());
|
||||
|
||||
el = document.createElement("a");
|
||||
el.setAttribute(
|
||||
"href",
|
||||
"data:text/plain;charset=utf-8," + btoa(dataStr),
|
||||
);
|
||||
el.setAttribute("target", "_blank");
|
||||
el.setAttribute("download", "sshwifty.known-remotes.txt");
|
||||
el.setAttribute(
|
||||
"style",
|
||||
"overflow: hidden; opacity: 0; width: 1px; height: 1px; top: -1px;" +
|
||||
"left: -1px; position: absolute;",
|
||||
);
|
||||
|
||||
document.body.appendChild(el);
|
||||
|
||||
el.click();
|
||||
} catch (e) {
|
||||
alert("Unable to export known remotes: " + e);
|
||||
}
|
||||
|
||||
if (el === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.removeChild(el);
|
||||
},
|
||||
importHosts() {
|
||||
const self = this;
|
||||
|
||||
let el = null;
|
||||
|
||||
try {
|
||||
el = document.createElement("input");
|
||||
el.setAttribute("type", "file");
|
||||
el.setAttribute(
|
||||
"style",
|
||||
"overflow: hidden; opacity: 0; width: 1px; height: 1px; top: -1px;" +
|
||||
"left: -1px; position: absolute;",
|
||||
);
|
||||
el.addEventListener("change", (ev) => {
|
||||
const t = ev.target;
|
||||
|
||||
if (t.files.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
t.disabled = "disabled";
|
||||
|
||||
let r = new FileReader();
|
||||
|
||||
r.onload = () => {
|
||||
try {
|
||||
self.knownsImport(JSON.parse(atob(r.result)));
|
||||
} catch (e) {
|
||||
alert("Unable to import known remotes due to error: " + e);
|
||||
}
|
||||
};
|
||||
|
||||
r.readAsText(t.files[0], "utf-8");
|
||||
});
|
||||
|
||||
document.body.appendChild(el);
|
||||
|
||||
el.click();
|
||||
} catch (e) {
|
||||
alert("Unable to load known remotes data due to error: " + e);
|
||||
}
|
||||
|
||||
if (el === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.removeChild(el);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,58 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect-new {
|
||||
min-height: 200px;
|
||||
background: #3a3a3a;
|
||||
font-size: 0.75em;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap:hover {
|
||||
background: #544;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap:active {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap {
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#connect-new li h2 {
|
||||
color: #e9a;
|
||||
}
|
||||
|
||||
#connect-new li h2::before {
|
||||
content: ">";
|
||||
margin: 0 5px 0 0;
|
||||
color: #555;
|
||||
font-weight: normal;
|
||||
transition: ease 0.3s margin;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap:hover h2::before {
|
||||
content: ">";
|
||||
margin: 0 3px 0 2px;
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="connect-new">
|
||||
<ul class="lst1 lst-nostyle">
|
||||
<li
|
||||
v-for="(connector, ck) in connectors"
|
||||
:key="ck"
|
||||
@click="select(connector)"
|
||||
>
|
||||
<div class="lst-wrap">
|
||||
<h2 :style="'color: ' + connector.color()">{{ connector.name() }}</h2>
|
||||
{{ connector.description() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect_new.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
connectors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
select(connector) {
|
||||
this.$emit("select", connector);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,56 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect-switch {
|
||||
font-size: 0.88em;
|
||||
color: #aaa;
|
||||
clear: both;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
#connect-switch li .label {
|
||||
padding: 2px 7px;
|
||||
margin-left: 3px;
|
||||
font-size: 0.85em;
|
||||
background: #444;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#connect-switch li.active {
|
||||
border-color: #555;
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
#connect-switch li.active .label {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
#connect-switch li.disabled {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#connect-switch.red {
|
||||
border-color: #a56;
|
||||
}
|
||||
|
||||
#connect-switch.red li.active {
|
||||
border-color: #a56;
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul id="connect-switch" class="tab2" :class="{ red: tab === 'known' }">
|
||||
<li :class="{ active: tab === 'new' }" @click="switchTab('new')">
|
||||
New remote
|
||||
</li>
|
||||
|
||||
<li :class="{ active: tab === 'known' }" @click="switchTab('known')">
|
||||
Known remotes <span class="label">{{ knownsLength }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect_switch.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tab: {
|
||||
type: String,
|
||||
default: "new",
|
||||
},
|
||||
knownsLength: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
switchTab(to) {
|
||||
this.$emit("switch", to);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,132 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="450" height="80">
|
||||
<path d="M104.9 44.333s22.069-11.953 49.294-11.953c27.23 0 49.298 11.953 49.298 11.953" fill="none" stroke="#d18b8d" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="100 150" stroke-dashoffset="100">
|
||||
<animate id="j" begin="0s;a.end+1s" attributeName="stroke-dashoffset" from="100" to="-1150" dur="2s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<path d="M203.49 32.393s-22.069 11.953-49.294 11.953c-27.226 0-49.294-11.953-49.294-11.953" fill="none" stroke="gray" stroke-width="3.166" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="100 150" stroke-dashoffset="100">
|
||||
<animate id="a" begin="b.end" attributeName="stroke-dashoffset" from="100" to="-1150" dur="2s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<path d="M267.6 37.257h64.101a7.51 7.51 0 1 0 0-15.02 7.51 7.51 0 0 0 0 15.02h31.753" fill="none" stroke="#d18b8d" stroke-width="3.166" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="100 150" stroke-dashoffset="100">
|
||||
<animate id="d" begin="c.end" attributeName="stroke-dashoffset" from="100" to="-1150" dur="2s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<path d="M366.36 37.288h-95.739" fill="none" stroke="gray" stroke-width="3.166" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="100 150" stroke-dashoffset="100">
|
||||
<animate id="k" begin="d.end+0.5s" attributeName="stroke-dashoffset" from="100" to="-1150" dur="2s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<g>
|
||||
<path d="M54.652 10.707c-5.462 0-9.914 4.452-9.914 9.914v21.844c0 5.462 4.452 9.914 9.914 9.914h32.762c5.462 0 9.914-4.452 9.914-9.914V20.62c0-5.462-4.452-9.914-9.914-9.914zm0 3.492h32.762c3.589 0 6.424 2.833 6.424 6.422v21.844c0 3.589-2.835 6.424-6.424 6.424H54.652c-3.588 0-6.423-2.835-6.423-6.424V20.62c0-3.589 2.835-6.422 6.423-6.422z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="gray"/>
|
||||
<g>
|
||||
<path d="M62.564 20.131H94.53a6.402 6.402 0 0 1 6.416 6.416v21.601a6.402 6.402 0 0 1-6.416 6.416H62.564a6.402 6.402 0 0 1-6.416-6.416V26.547a6.402 6.402 0 0 1 6.416-6.416z" fill="#262626"/>
|
||||
<g>
|
||||
<path d="M59.058 16.727h30.067a7.35 7.35 0 0 1 7.365 7.365v19.702a7.35 7.35 0 0 1-7.365 7.366H59.058a7.35 7.35 0 0 1-7.366-7.366V24.092a7.35 7.35 0 0 1 7.366-7.365z" fill="#d18b8d"/>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M91.056 45.224a3.17 3.17 0 0 1-3.17 3.17 3.17 3.17 0 0 1-3.17-3.17 3.17 3.17 0 0 1 3.17-3.17 3.17 3.17 0 0 1 3.17 3.17z" fill="#fff">
|
||||
<animate id="e" begin="3s;e.end+10s" attributeName="d" from="m 91.056202,45.223999 a 3.1702001,3.1702001 0 0 1 -3.1702,3.1702 3.1702001,3.1702001 0 0 1 -3.170201,-3.1702 3.1702001,3.1702001 0 0 1 3.170201,-3.1702 3.1702001,3.1702001 0 0 1 3.1702,3.1702 z" to="m 88.773133,45.223999 a 0.8871316,0.8871316 0 0 1 -0.887131,0.887132 0.8871316,0.8871316 0 0 1 -0.887132,-0.887132 0.8871316,0.8871316 0 0 1 0.887132,-0.887132 0.8871316,0.8871316 0 0 1 0.887131,0.887132 z" dur="0.1s" calcMode="linear"/>
|
||||
</path>
|
||||
<path d="M67.432 45.224a3.17 3.17 0 0 1-3.17 3.17 3.17 3.17 0 0 1-3.17-3.17 3.17 3.17 0 0 1 3.17-3.17 3.17 3.17 0 0 1 3.17 3.17z" fill="#fff">
|
||||
<animate begin="e.begin" end="r1fe1.end" attributeName="d" from="m 67.432201,45.223999 a 3.1702001,3.1702001 0 0 1 -3.1702,3.1702 3.1702001,3.1702001 0 0 1 -3.1702,-3.1702 3.1702001,3.1702001 0 0 1 3.1702,-3.1702 3.1702001,3.1702001 0 0 1 3.1702,3.1702 z" to="m 65.149133,45.223999 a 0.8871316,0.8871316 0 0 1 -0.887132,0.887132 0.8871316,0.8871316 0 0 1 -0.887132,-0.887132 0.8871316,0.8871316 0 0 1 0.887132,-0.887132 0.8871316,0.8871316 0 0 1 0.887132,0.887132 z" dur="0.1s" calcMode="linear"/>
|
||||
</path>
|
||||
<animateMotion id="f" begin="1s;f.end+2s" from="0.3,0.5" to="0,0.3" dur="5s" calcMode="discrete"/>
|
||||
</g>
|
||||
<path d="M72.898 44.274h5.485c.741 0 1.338.597 1.338 1.338v.004c0 .74-.597 1.337-1.338 1.337h-5.485a1.335 1.335 0 0 1-1.338-1.337v-.004c0-.741.597-1.338 1.338-1.338z" fill="#fff"/>
|
||||
<animateMotion begin="f.begin" end="r1ff1b.end" from="1.2,0.5" to="0,1.2" dur="5s" calcMode="discrete"/>
|
||||
</g>
|
||||
<path d="M65.781 21.2H85.82a3.902 3.902 0 0 1 3.91 3.91V36.11a3.902 3.902 0 0 1-3.91 3.91H65.78a3.902 3.902 0 0 1-3.91-3.91V25.11a3.902 3.902 0 0 1 3.91-3.91z" fill="#ed9499"/>
|
||||
<path d="M68.03 24.089h18.174a3.298 3.298 0 0 1 3.305 3.305v9.304a3.298 3.298 0 0 1-3.305 3.305H68.03a3.298 3.298 0 0 1-3.305-3.305v-9.304a3.298 3.298 0 0 1 3.305-3.305z" fill="#cc7f80"/>
|
||||
<g fill="#f5d0d0">
|
||||
<path d="M68.437 27.374h17.787a.73.73 0 1 1 0 1.46H68.437a.73.73 0 1 1 0-1.46zM68.437 31.213h17.787a.73.73 0 1 1 0 1.46H68.437a.73.73 0 1 1 0-1.46zM68.437 35.052h17.787a.73.73 0 1 1 0 1.46H68.437a.73.73 0 1 1 0-1.46z"/>
|
||||
</g>
|
||||
<animateMotion begin="f.begin" from="0,0" to="1,0" dur="0.2s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="f.end" from="1,0" to="0,0" dur="0.2s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion id="h" begin="g.begin;r1ba2.end" from="0,0" to="0,2" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="h.end" from="0,2" to="0,0" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion id="g" begin="0s;i.end" from="0,0" to="0,3" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion id="i" begin="g.end" from="0,3" to="0,0" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<path d="M55.93 74.551h31.663c2.193 0 3.958 1.06 3.958 2.375S89.786 79.3 87.593 79.3H55.931c-2.193 0-3.958-1.059-3.958-2.374 0-1.316 1.765-2.375 3.958-2.375z" fill="#262626">
|
||||
<animate begin="g.begin" end="r1bb1.end" attributeName="d" from="m 60.268629,74.551003 h 22.98674 c 1.591832,0 2.873342,1.059116 2.873342,2.3747 0,1.315583 -1.28151,2.3747 -2.873342,2.3747 h -22.98674 c -1.591831,0 -2.873341,-1.059117 -2.873341,-2.3747 0,-1.315584 1.28151,-2.3747 2.873341,-2.3747 z" to="m 55.930799,74.551003 h 31.6624 c 2.192621,0 3.9578,1.059116 3.9578,2.3747 0,1.315583 -1.765179,2.3747 -3.9578,2.3747 h -31.6624 c -2.192621,0 -3.957799,-1.059117 -3.957799,-2.3747 0,-1.315584 1.765178,-2.3747 3.957799,-2.3747 z" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
<animate begin="i.begin" end="r1bb2.end" attributeName="d" from="m 55.930799,74.551003 h 31.6624 c 2.192621,0 3.9578,1.059116 3.9578,2.3747 0,1.315583 -1.765179,2.3747 -3.9578,2.3747 h -31.6624 c -2.192621,0 -3.957799,-1.059117 -3.957799,-2.3747 0,-1.315584 1.765178,-2.3747 3.957799,-2.3747 z" to="m 60.268629,74.551003 h 22.98674 c 1.591832,0 2.873342,1.059116 2.873342,2.3747 0,1.315583 -1.28151,2.3747 -2.873342,2.3747 h -22.98674 c -1.591831,0 -2.873341,-1.059117 -2.873341,-2.3747 0,-1.315584 1.28151,-2.3747 2.873341,-2.3747 z" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<g>
|
||||
<path d="M228.714 3.548h18.122c8.323 0 15.024 6.7 15.024 15.024v29.64c0 8.323-6.7 15.024-15.024 15.024h-18.122c-8.323 0-15.024-6.7-15.024-15.024v-29.64c0-8.323 6.7-15.024 15.024-15.024z" fill="#262626"/>
|
||||
<g>
|
||||
<path style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" d="M225.08 2.084c-7.337 0-13.293 5.955-13.293 13.293v30.227c0 7.337 5.956 13.294 13.293 13.294h19.756c7.337 0 13.293-5.957 13.293-13.294V15.377c0-7.338-5.956-13.293-13.293-13.293zm0 3.166h19.756a10.07 10.07 0 0 1 10.127 10.127v30.227a10.071 10.071 0 0 1-10.127 10.128H225.08a10.071 10.071 0 0 1-10.127-10.128V15.377A10.07 10.07 0 0 1 225.08 5.25z" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="gray"/>
|
||||
<g>
|
||||
<path d="M230.055 43.997h10.444a3.657 3.657 0 0 1 3.665 3.665 3.657 3.657 0 0 1-3.665 3.665h-10.444a3.657 3.657 0 0 1-3.665-3.665 3.657 3.657 0 0 1 3.665-3.665zM225.185 47.638a3.665 3.665 0 0 1-3.665 3.665 3.665 3.665 0 0 1-3.665-3.665 3.665 3.665 0 0 1 3.665-3.665 3.665 3.665 0 0 1 3.665 3.665zM225.961 8.06h10.069c4.499 0 8.121 3.622 8.121 8.121v8.89c0 4.499-3.622 8.12-8.121 8.12H225.96a8.104 8.104 0 0 1-8.121-8.12v-8.89c0-4.499 3.622-8.121 8.121-8.121z" fill="#d18b8d"/>
|
||||
<g>
|
||||
<path d="M224.068 12.572a2.263 2.263 0 0 1 2.268 2.268v3.38a2.263 2.263 0 0 1-2.268 2.268 2.263 2.263 0 0 1-2.268-2.268v-3.38a2.263 2.263 0 0 1 2.268-2.268z" fill="#f9f9f9">
|
||||
<animate begin="c.begin" attributeName="d" from="m 224.0678,12.572 c 1.25636,0 2.2678,1.011438 2.2678,2.2678 v 3.379999 c 0,1.256362 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011438 -2.2678,-2.2678 V 14.8398 c 0,-1.256362 1.01144,-2.2678 2.2678,-2.2678 z" to="m 224.0678,15.498051 c 1.25636,0 2.2678,1.011438 2.2678,2.2678 v 0.453948 c 0,1.256362 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011438 -2.2678,-2.2678 v -0.453948 c 0,-1.256362 1.01144,-2.2678 2.2678,-2.2678 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animate begin="b.begin" attributeName="d" from="m 224.0678,15.498051 c 1.25636,0 2.2678,1.011438 2.2678,2.2678 v 0.453948 c 0,1.256362 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011438 -2.2678,-2.2678 v -0.453948 c 0,-1.256362 1.01144,-2.2678 2.2678,-2.2678 z" to="m 224.0678,12.572 c 1.25636,0 2.2678,1.011438 2.2678,2.2678 v 3.379999 c 0,1.256362 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011438 -2.2678,-2.2678 V 14.8398 c 0,-1.256362 1.01144,-2.2678 2.2678,-2.2678 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<path d="M235.108 15.342a2.263 2.263 0 0 1 2.268 2.268v.61a2.263 2.263 0 0 1-2.268 2.267 2.263 2.263 0 0 1-2.268-2.268v-.61a2.263 2.263 0 0 1 2.268-2.267z" fill="#f9f9f9">
|
||||
<animate begin="c.begin" attributeName="d" from="m 235.1078,15.342 c 1.25636,0 2.2678,1.011439 2.2678,2.2678 v 0.6095 c 0,1.256361 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011439 -2.2678,-2.2678 v -0.6095 c 0,-1.256361 1.01144,-2.2678 2.2678,-2.2678 z" to="m 235.1078,12.819542 c 1.25636,0 2.2678,1.011439 2.2678,2.2678 V 18.2193 c 0,1.256361 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011439 -2.2678,-2.2678 v -3.131958 c 0,-1.256361 1.01144,-2.2678 2.2678,-2.2678 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animate begin="b.begin" attributeName="d" from="m 235.1078,12.819542 c 1.25636,0 2.2678,1.011439 2.2678,2.2678 V 18.2193 c 0,1.256361 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011439 -2.2678,-2.2678 v -3.131958 c 0,-1.256361 1.01144,-2.2678 2.2678,-2.2678 z" to="m 235.1078,15.342 c 1.25636,0 2.2678,1.011439 2.2678,2.2678 v 0.6095 c 0,1.256361 -1.01144,2.2678 -2.2678,2.2678 -1.25636,0 -2.2678,-1.011439 -2.2678,-2.2678 v -0.6095 c 0,-1.256361 1.01144,-2.2678 2.2678,-2.2678 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<animateMotion begin="c.begin" from="0,0" to="5,0" dur="0.2s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="b.begin" from="5,0" to="0,0" dur="0.2s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<path d="M219.554 36.873h22.883c.95 0 1.714.763 1.714 1.712 0 .948-.764 1.712-1.714 1.712h-22.883c-.95 0-1.714-.764-1.714-1.712 0-.949.764-1.712 1.714-1.712z" fill="gray"/>
|
||||
<animateMotion id="c" begin="j.end" from="0,0" to="7,0" dur="0.2s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion id="b" begin="k.end" from="7,0" to="0,0" dur="0.2s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion begin="l.begin" from="0,0" to="0,2" dur="1.5s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="m.begin" from="0,2" to="0,0" dur="1.5s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion id="l" begin="0;m.end" from="0,0" to="0,2" dur="1.7s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion id="m" begin="l.end" from="0,2" to="0,0" dur="1.7s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<path d="M222.158 74.551h27.704c2.193 0 3.958 1.06 3.958 2.375s-1.765 2.374-3.958 2.374h-27.704c-2.193 0-3.958-1.059-3.958-2.374 0-1.316 1.765-2.375 3.958-2.375z" fill="#262626">
|
||||
<animate begin="l.begin" end="r3ba1.end" attributeName="d" from="m 223.41342,74.551003 h 25.19316 c 1.99387,0 3.59905,1.059116 3.59905,2.3747 0,1.315583 -1.60518,2.3747 -3.59905,2.3747 h -25.19316 c -1.99387,0 -3.59905,-1.059117 -3.59905,-2.3747 0,-1.315584 1.60518,-2.3747 3.59905,-2.3747 z" to="m 222.1578,74.551003 h 27.7044 c 2.19262,0 3.9578,1.059116 3.9578,2.3747 0,1.315583 -1.76518,2.3747 -3.9578,2.3747 h -27.7044 c -2.19262,0 -3.9578,-1.059117 -3.9578,-2.3747 0,-1.315584 1.76518,-2.3747 3.9578,-2.3747 z" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
<animate begin="m.begin" end="r3ba2.end" attributeName="d" from="m 222.1578,74.551003 h 27.7044 c 2.19262,0 3.9578,1.059116 3.9578,2.3747 0,1.315583 -1.76518,2.3747 -3.9578,2.3747 h -27.7044 c -2.19262,0 -3.9578,-1.059117 -3.9578,-2.3747 0,-1.315584 1.76518,-2.3747 3.9578,-2.3747 z" to="m 223.41342,74.551003 h 25.19316 c 1.99387,0 3.59905,1.059116 3.59905,2.3747 0,1.315583 -1.60518,2.3747 -3.59905,2.3747 h -25.19316 c -1.99387,0 -3.59905,-1.059117 -3.59905,-2.3747 0,-1.315584 1.60518,-2.3747 3.59905,-2.3747 z" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M381.245 15.105h23.224a5.892 5.892 0 0 1 5.905 5.905v25.472a5.892 5.892 0 0 1-5.905 5.905h-23.224a5.892 5.892 0 0 1-5.905-5.905V21.01a5.892 5.892 0 0 1 5.905-5.905z" fill="#262626"/>
|
||||
<g>
|
||||
<path d="M377.783 14.155h23.968a3.854 3.854 0 0 1 3.863 3.863V44.02a3.854 3.854 0 0 1-3.863 3.863h-23.968a3.854 3.854 0 0 1-3.863-3.863V18.018a3.854 3.854 0 0 1 3.863-3.863z" fill="gray" stroke="gray" stroke-width="2.486"/>
|
||||
<g>
|
||||
<path d="M378.21 16.332h20.984a3.373 3.373 0 0 1 3.38 3.38v22.765a3.373 3.373 0 0 1-3.38 3.38H378.21a3.373 3.373 0 0 1-3.38-3.38V19.712a3.373 3.373 0 0 1 3.38-3.38z" fill="#262626"/>
|
||||
<g>
|
||||
<path d="M380.116 19.142h18.606a2.99 2.99 0 0 1 2.996 2.996v20.185a2.99 2.99 0 0 1-2.996 2.996h-18.606a2.99 2.99 0 0 1-2.996-2.996V22.138a2.99 2.99 0 0 1 2.996-2.996z" fill="#848484"/>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M382.1 23.891h2.426c.61 0 1.1.49 1.1 1.1v1.318c0 .61-.49 1.1-1.1 1.1H382.1c-.61 0-1.1-.49-1.1-1.1v-1.318c0-.61.49-1.1 1.1-1.1z" fill="#fff">
|
||||
<animate id="o" begin="5s;n.end+3s" attributeName="d" from="m 382.1003,23.891001 h 2.4261 c 0.60957,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49073,1.1003 -1.1003,1.1003 h -2.4261 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" to="m 382.1003,23.891001 4.04047,0 c 0.60957,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49073,1.1003 -1.1003,1.1003 l -4.04047,0 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animate id="n" begin="o.end+5s" attributeName="d" from="m 382.1003,23.891001 4.04047,0 c 0.60957,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49073,1.1003 -1.1003,1.1003 l -4.04047,0 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" to="m 382.1003,23.891001 h 2.4261 c 0.60957,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49073,1.1003 -1.1003,1.1003 h -2.4261 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<path d="M389.42 23.891h6.111c.61 0 1.1.49 1.1 1.1v1.318c0 .61-.49 1.1-1.1 1.1h-6.11c-.61 0-1.101-.49-1.101-1.1v-1.318c0-.61.49-1.1 1.1-1.1z" fill="#fff">
|
||||
<animate begin="o.begin" attributeName="d" from="m 389.42031,23.891001 h 6.1108 c 0.60956,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49074,1.1003 -1.1003,1.1003 h -6.1108 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" to="m 391.43827,23.891001 h 4.09284 c 0.60956,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49074,1.1003 -1.1003,1.1003 h -4.09284 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animate begin="n.begin" attributeName="d" from="m 391.43827,23.891001 h 4.09284 c 0.60956,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49074,1.1003 -1.1003,1.1003 h -4.09284 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" to="m 389.42031,23.891001 h 6.1108 c 0.60956,0 1.1003,0.490734 1.1003,1.1003 v 1.3179 c 0,0.609566 -0.49074,1.1003 -1.1003,1.1003 h -6.1108 c -0.60957,0 -1.1003,-0.490734 -1.1003,-1.1003 v -1.3179 c 0,-0.609566 0.49073,-1.1003 1.1003,-1.1003 z" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
<animateMotion id="p" begin="6s;p.end+5s" from="0,0" to="0,1" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="p.end+2s" from="0,1" to="0,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M393.047 37.15a5.877 5.877 0 0 1-5.877 5.877 5.877 5.877 0 0 1-5.877-5.877 5.877 5.877 0 0 1 5.877-5.877 5.877 5.877 0 0 1 5.877 5.877z" fill="#262626"/>
|
||||
<path d="M390.68 37.11a3.51 3.51 0 0 1-3.51 3.51 3.51 3.51 0 0 1-3.51-3.51 3.51 3.51 0 0 1 3.51-3.51 3.51 3.51 0 0 1 3.51 3.51z" fill="#d18b8d"/>
|
||||
<path style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" d="M386.46 30.879c-3.146 0-5.718 2.57-5.718 5.717s2.572 5.717 5.719 5.717c3.147 0 5.717-2.57 5.717-5.717 0-3.148-2.57-5.717-5.717-5.717zm0 1.697c2.23 0 4.019 1.79 4.019 4.02 0 2.23-1.788 4.02-4.018 4.02-2.23 0-4.02-1.79-4.02-4.02 0-2.23 1.79-4.02 4.02-4.02z" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#aaa"/>
|
||||
<animateMotion begin="o.begin" from="0,0" to="1,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="n.begin" from="1,0" to="0,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion begin="o.begin" from="0,0" to="1,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="n.begin" from="1,0" to="0,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion begin="o.begin" from="0,0" to="-1,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="n.begin" from="-1,0" to="0,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion begin="o.begin" from="0,0" to="1.3,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="n.begin" from="1.3,0" to="0,0" dur="0.1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion id="r" begin="0s;q.end" from="0,0" to="0,1" dur="1.1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion id="q" begin="r.end" from="0,1" to="0,0" dur="1.1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<animateMotion begin="r.begin" from="0,0" to="0,2" dur="1.1s" fill="freeze" calcMode="linear"/>
|
||||
<animateMotion begin="q.begin" from="0,2" to="0,0" dur="1.1s" fill="freeze" calcMode="linear"/>
|
||||
</g>
|
||||
<path d="M380.468 74.551h19.79c2.192 0 3.957 1.06 3.957 2.375s-1.765 2.374-3.958 2.374h-19.79c-2.192 0-3.957-1.059-3.957-2.374 0-1.316 1.765-2.375 3.958-2.375z" fill="#262626">
|
||||
<animate begin="r.begin" end="r2ba1.end" attributeName="d" from="m 382.77407,74.551003 h 15.17688 c 1.68156,0 3.03531,1.059116 3.03531,2.3747 0,1.315583 -1.35375,2.3747 -3.03531,2.3747 h -15.17688 c -1.68156,0 -3.03531,-1.059117 -3.03531,-2.3747 0,-1.315584 1.35375,-2.3747 3.03531,-2.3747 z" to="m 380.46781,74.551003 h 19.7894 c 2.19262,0 3.9578,1.059116 3.9578,2.3747 0,1.315583 -1.76518,2.3747 -3.9578,2.3747 h -19.7894 c -2.19262,0 -3.9578,-1.059117 -3.9578,-2.3747 0,-1.315584 1.76518,-2.3747 3.9578,-2.3747 z" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
<animate begin="q.begin" end="r2ba2.end" attributeName="d" from="m 380.46781,74.551003 h 19.7894 c 2.19262,0 3.9578,1.059116 3.9578,2.3747 0,1.315583 -1.76518,2.3747 -3.9578,2.3747 h -19.7894 c -2.19262,0 -3.9578,-1.059117 -3.9578,-2.3747 0,-1.315584 1.76518,-2.3747 3.9578,-2.3747 z" to="m 382.77407,74.551003 h 15.17688 c 1.68156,0 3.03531,1.059116 3.03531,2.3747 0,1.315583 -1.35375,2.3747 -3.03531,2.3747 h -15.17688 c -1.68156,0 -3.03531,-1.059117 -3.03531,-2.3747 0,-1.315584 1.35375,-2.3747 3.03531,-2.3747 z" dur="1s" fill="freeze" calcMode="linear"/>
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 22 KiB |
@ -1,124 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connector {
|
||||
padding: 0 20px 40px 20px;
|
||||
}
|
||||
|
||||
#connector-cancel {
|
||||
text-decoration: none;
|
||||
color: #e9a;
|
||||
}
|
||||
|
||||
#connector-cancel.disabled {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
#connector-cancel::before {
|
||||
content: "\000AB";
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
#connector-title {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#connector-title > h2 {
|
||||
color: #e9a;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
#connector-title.big {
|
||||
margin: 50px 0;
|
||||
}
|
||||
|
||||
#connector-title.big > h2 {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#connector-fields {
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#connector-continue {
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#connector-proccess {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#connector-proccess-message {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
#connector-proccess-message > h2 {
|
||||
font-weight: normal;
|
||||
margin: 10px 0;
|
||||
color: #e9a;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
#connector-proccess-message > h2 > span {
|
||||
padding: 2px 10px;
|
||||
border: 2px solid transparent;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes connector-proccess-message-alert {
|
||||
0% {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
outline: 2px solid #e9a;
|
||||
}
|
||||
|
||||
60% {
|
||||
border-color: #e9a;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
#connector-proccess-message.alert > h2 > span {
|
||||
outline: 2px solid transparent;
|
||||
animation-name: connector-proccess-message-alert;
|
||||
animation-duration: 1.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
|
||||
#connector-proccess-indicater {
|
||||
width: 100%;
|
||||
margin: 20px auto;
|
||||
padding: 0;
|
||||
}
|
||||
@ -1,834 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<form
|
||||
id="connector"
|
||||
class="form1"
|
||||
action="javascript:;"
|
||||
method="POST"
|
||||
@submit="submitAndGetNext"
|
||||
>
|
||||
<a
|
||||
id="connector-cancel"
|
||||
href="javascript:;"
|
||||
:class="{ disabled: working || cancelled }"
|
||||
@click="cancel()"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-if="!working"
|
||||
id="connector-title"
|
||||
:class="{ big: current.fields.length <= 0 }"
|
||||
>
|
||||
<h2>{{ current.title || connector.name }}</h2>
|
||||
|
||||
<p>{{ current.message || connector.description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="working" id="connector-proccess">
|
||||
<img id="connector-proccess-indicater" src="./connecting.svg" />
|
||||
|
||||
<div id="connector-proccess-message" :class="{ alert: current.alert }">
|
||||
<h2>
|
||||
<span>{{ current.title || connector.name }}</span>
|
||||
</h2>
|
||||
|
||||
<p>{{ current.message || connector.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset id="connector-fields">
|
||||
<div
|
||||
v-for="(field, key) in current.fields"
|
||||
:key="key"
|
||||
class="field"
|
||||
:class="{ error: field.error.length > 0, highlight: field.highlighted }"
|
||||
>
|
||||
{{ field.field.name }}
|
||||
|
||||
<input
|
||||
v-if="field.field.type === 'text'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:placeholder="field.field.example"
|
||||
:autofocus="field.autofocus"
|
||||
:tabindex="field.tabIndex"
|
||||
:disabled="field.field.readonly"
|
||||
@keydown="triggerSuggestions($event, key, field)"
|
||||
@focus="focus(key, field, true)"
|
||||
@blur="focus(key, field, false)"
|
||||
@input="changed(key, field, false)"
|
||||
@change="changed(key, field, true)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-if="field.field.type === 'password'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:placeholder="field.field.example"
|
||||
:autofocus="field.autofocus"
|
||||
:tabindex="field.tabIndex"
|
||||
:disabled="field.field.readonly"
|
||||
@focus="focus(key, field, true)"
|
||||
@blur="focus(key, field, false)"
|
||||
@input="changed(key, field, false)"
|
||||
@change="changed(key, field, true)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-if="field.field.type === 'checkbox'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
type="checkbox"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:autofocus="field.autofocus"
|
||||
:tabindex="field.tabIndex"
|
||||
:disabled="field.field.readonly"
|
||||
@focus="focus(key, field, true)"
|
||||
@blur="focus(key, field, false)"
|
||||
@input="changed(key, field, false)"
|
||||
@change="changed(key, field, true)"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
v-if="field.field.type === 'textarea'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
autocomplete="off"
|
||||
:placeholder="field.field.example"
|
||||
:name="field.field.name"
|
||||
:autofocus="field.autofocus"
|
||||
:tabindex="field.tabIndex"
|
||||
:disabled="field.field.readonly"
|
||||
@keyup="expandTextarea($event)"
|
||||
@keydown="
|
||||
triggerSuggestions($event, key, field) || expandTextarea($event)
|
||||
"
|
||||
@focus="focus(key, field, true)"
|
||||
@blur="focus(key, field, false)"
|
||||
@input="changed(key, field, false)"
|
||||
@change="changed(key, field, true)"
|
||||
></textarea>
|
||||
|
||||
<input
|
||||
v-if="field.field.type === 'textfile'"
|
||||
v-focus="field.autofocus"
|
||||
type="file"
|
||||
autocomplete="off"
|
||||
:placeholder="field.field.example"
|
||||
:name="field.field.name + '-file'"
|
||||
:autofocus="field.autofocus"
|
||||
:tabindex="field.tabIndex"
|
||||
:disabled="field.field.readonly"
|
||||
@focus="focus(key, field, true)"
|
||||
@blur="focus(key, field, false)"
|
||||
@change="importFile($event.target, field)"
|
||||
/>
|
||||
<input
|
||||
v-if="field.field.type === 'textfile'"
|
||||
v-model="field.field.value"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:placeholder="field.field.example"
|
||||
style="display: none"
|
||||
@input="changed(key, field, false)"
|
||||
@change="changed(key, field, true)"
|
||||
/>
|
||||
|
||||
<select
|
||||
v-if="field.field.type === 'select'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:autofocus="field.autofocus"
|
||||
:value="field.field.value"
|
||||
:tabindex="field.tabIndex"
|
||||
:disabled="field.field.readonly"
|
||||
@focus="focus(key, field, true)"
|
||||
@blur="focus(key, field, false)"
|
||||
@input="changed(key, field, false)"
|
||||
@change="changed(key, field, true)"
|
||||
>
|
||||
<option
|
||||
v-for="(option, oKey) in field.field.example.split(',')"
|
||||
:key="oKey"
|
||||
:value="option"
|
||||
:selected="field.field.value === option"
|
||||
:aria-selected="field.field.value === option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div v-if="field.field.type === 'textdata'" class="textinfo">
|
||||
<div class="info">{{ field.field.value }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="field.field.type === 'radio'" class="items">
|
||||
<label
|
||||
v-for="(option, oKey) in field.field.example.split(',')"
|
||||
:key="oKey"
|
||||
class="field horizontal item"
|
||||
>
|
||||
<input
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus && oKey === 0"
|
||||
type="radio"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:autofocus="field.autofocus && oKey === 0"
|
||||
:value="option"
|
||||
:checked="field.field.value === option"
|
||||
:aria-checked="field.field.value === option"
|
||||
:tabindex="field.nextSubTabIndex(oKey)"
|
||||
:disabled="field.field.readonly"
|
||||
@focus="focus(key, field, true)"
|
||||
@blur="focus(key, field, false)"
|
||||
@input="changed(key, field, false)"
|
||||
@change="changed(key, field, true)"
|
||||
/>
|
||||
{{ option }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-if="field.suggestion.suggestions.length > 0"
|
||||
class="input-suggestions lst-nostyle"
|
||||
@mouseenter="field.holdSuggestions(true)"
|
||||
@mouseleave="field.holdSuggestions(false)"
|
||||
>
|
||||
<li
|
||||
v-for="(suggestion, sKey) in field.suggestion.suggestions"
|
||||
:key="sKey"
|
||||
:class="{ current: sKey === field.suggestion.selected }"
|
||||
@click="clickInputSuggestion(key, field, sKey)"
|
||||
>
|
||||
<div class="sugt-title">
|
||||
{{ suggestion.title }}
|
||||
</div>
|
||||
<div class="sugt-value">
|
||||
{{ suggestion.value }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="field.error.length > 0" class="error">{{ field.error }}</div>
|
||||
<div v-else-if="field.message.length > 0" class="message">
|
||||
{{ field.message }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="field.field.description.length > 0"
|
||||
class="message"
|
||||
v-html="field.field.description"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<button
|
||||
v-if="current.submittable"
|
||||
v-focus="submitterTabIndex === 1"
|
||||
type="submit"
|
||||
:disabled="current.submitting || disabled"
|
||||
:tabindex="submitterTabIndex"
|
||||
:autofocus="submitterTabIndex === 1"
|
||||
@click="submitAndGetNext"
|
||||
>
|
||||
{{ current.actionText }}
|
||||
</button>
|
||||
<button
|
||||
v-if="current.cancellable"
|
||||
:disabled="current.submitting || disabled"
|
||||
:tabindex="submitterTabIndex + 1"
|
||||
class="secondary"
|
||||
@click="cancelAndGetNext"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div
|
||||
v-if="preloaderIDName.length > 0"
|
||||
style="
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<div :id="preloaderIDName">
|
||||
{{ current.title || connector.name }} wizard
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connector.css";
|
||||
import * as command from "../commands/commands.js";
|
||||
import * as fieldBuilder from "./connector_field_builder.js";
|
||||
|
||||
const preloaderIDPrefix = "connector-resource-preload-control-";
|
||||
const hightlightClearTimeout = 1000;
|
||||
|
||||
function buildEmptyCurrent() {
|
||||
return {
|
||||
data: null,
|
||||
alert: false,
|
||||
clearHightlightTimeout: null,
|
||||
title: "",
|
||||
message: "",
|
||||
fields: [],
|
||||
actionText: "Continue",
|
||||
cancellable: false,
|
||||
submittable: false,
|
||||
submitting: false,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
focus: {
|
||||
inserted(el, binding) {
|
||||
if (!binding.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.focus();
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
connector: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentConnector: null,
|
||||
currentConnectorCloseWait: null,
|
||||
current: buildEmptyCurrent(),
|
||||
preloaderIDName: "",
|
||||
fieldValueBackup: [],
|
||||
submitterTabIndex: 1,
|
||||
working: false,
|
||||
disabled: false,
|
||||
cancelled: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
async connector(oldV, newV) {
|
||||
if (this.currentConnector !== null) {
|
||||
await this.closeWizard();
|
||||
}
|
||||
|
||||
this.cancelled = false;
|
||||
this.currentConnector = newV;
|
||||
this.runWizard();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.closeWizard();
|
||||
this.runWizard();
|
||||
this.cancelled = false;
|
||||
},
|
||||
async beforeDestroy() {
|
||||
try {
|
||||
await this.closeWizard();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async sendCancel() {
|
||||
await this.closeWizard();
|
||||
|
||||
this.$emit("cancel", true);
|
||||
},
|
||||
cancel() {
|
||||
if (this.working) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelled = true;
|
||||
|
||||
this.sendCancel();
|
||||
},
|
||||
buildCurrent(next) {
|
||||
try {
|
||||
this.current = buildEmptyCurrent();
|
||||
|
||||
this.working = this.getConnector().wizard.started();
|
||||
|
||||
this.current.type = next.type();
|
||||
this.current.data = next.data();
|
||||
|
||||
let fields = null,
|
||||
tabIndex = 1;
|
||||
|
||||
switch (this.current.type) {
|
||||
case command.NEXT_PROMPT:
|
||||
fields = this.current.data.inputs();
|
||||
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const f = fieldBuilder.build(tabIndex, i, fields[i]);
|
||||
if (f.field.readonly) {
|
||||
this.verify(i, f, true);
|
||||
}
|
||||
this.current.fields.push(f);
|
||||
tabIndex = f.nextTabIndex();
|
||||
}
|
||||
|
||||
this.submitterTabIndex = tabIndex > 0 ? tabIndex : 1;
|
||||
this.current.actionText = this.current.data.actionText();
|
||||
this.current.submittable = true;
|
||||
this.current.alert = true;
|
||||
this.current.cancellable = true;
|
||||
|
||||
// Fallthrough
|
||||
|
||||
case command.NEXT_WAIT:
|
||||
this.current.title = this.current.data.title();
|
||||
this.current.message = this.current.data.message();
|
||||
break;
|
||||
|
||||
case command.NEXT_DONE:
|
||||
this.working = false;
|
||||
this.disabled = true;
|
||||
|
||||
if (!this.current.data.success()) {
|
||||
this.current.title = this.current.data.error();
|
||||
this.current.message = this.current.data.message();
|
||||
} else {
|
||||
this.$emit("done", this.current.data.data());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown command type");
|
||||
}
|
||||
|
||||
if (!this.working) {
|
||||
this.current.cancellable = false;
|
||||
}
|
||||
|
||||
return next;
|
||||
} catch (e) {
|
||||
this.current.title = "Encountered an error";
|
||||
this.current.message = e;
|
||||
|
||||
this.working = false;
|
||||
this.disabled = true;
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
getConnector() {
|
||||
if (this.currentConnector === null) {
|
||||
this.currentConnector = this.connector;
|
||||
}
|
||||
|
||||
return this.currentConnector;
|
||||
},
|
||||
async closeWizard() {
|
||||
if (this.currentConnectorCloseWait === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let waiter = this.currentConnectorCloseWait;
|
||||
|
||||
this.currentConnectorCloseWait = null;
|
||||
|
||||
this.getConnector().wizard.close();
|
||||
|
||||
try {
|
||||
await waiter;
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
},
|
||||
runWizard() {
|
||||
if (this.currentConnectorCloseWait !== null) {
|
||||
throw new Error("Cannot run wizard multiple times");
|
||||
}
|
||||
|
||||
this.preloaderIDName =
|
||||
preloaderIDPrefix +
|
||||
this.getConnector().wizard.control().ui().toLowerCase();
|
||||
|
||||
this.currentConnectorCloseWait = (async () => {
|
||||
while (!this.disabled) {
|
||||
this.clearFieldValueBackup();
|
||||
|
||||
let next = this.buildCurrent(await this.getConnector().wizard.next());
|
||||
|
||||
switch (next.type()) {
|
||||
case command.NEXT_PROMPT:
|
||||
case command.NEXT_WAIT:
|
||||
continue;
|
||||
case command.NEXT_DONE:
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown command type");
|
||||
}
|
||||
}
|
||||
})();
|
||||
},
|
||||
getFieldValues() {
|
||||
let mod = {};
|
||||
|
||||
for (let i = 0; i < this.current.fields.length; i++) {
|
||||
mod[this.current.fields[i].field.name] =
|
||||
this.current.fields[i].field.value;
|
||||
}
|
||||
|
||||
return mod;
|
||||
},
|
||||
createFieldValueBackup() {
|
||||
let backup = [];
|
||||
|
||||
for (let i = 0; i < this.current.fields.length; i++) {
|
||||
backup.push(this.current.fields[i].field.value);
|
||||
}
|
||||
|
||||
this.fieldValueBackup = backup;
|
||||
},
|
||||
clearFieldValueBackup() {
|
||||
this.fieldValueBackup = [];
|
||||
},
|
||||
clearFieldHighlights() {
|
||||
for (let i = 0; i < this.current.fields.length; i++) {
|
||||
this.current.fields[i].highlighted = false;
|
||||
}
|
||||
},
|
||||
delayedClearFieldHighlights(timeout) {
|
||||
const self = this;
|
||||
|
||||
if (self.clearHightlightTimeout === null) {
|
||||
clearTimeout(self.clearHightlightTimeout);
|
||||
self.clearHightlightTimeout = null;
|
||||
}
|
||||
|
||||
self.clearHightlightTimeout = setTimeout(() => {
|
||||
self.clearHightlightTimeout = null;
|
||||
self.clearFieldHighlights();
|
||||
}, timeout);
|
||||
},
|
||||
restoreFieldValuesFromBackup(except) {
|
||||
for (let i = 0; i < this.fieldValueBackup.length; i++) {
|
||||
if (except === i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.current.fields[i].field.value = this.fieldValueBackup[i];
|
||||
}
|
||||
},
|
||||
expandTextarea(event) {
|
||||
// WARNING: This function may cause rendering stutter due to
|
||||
// combined problem of CSS "Position" and Vue render.
|
||||
// Use of "TextArea" element is thus not recommended.
|
||||
event.target.style.height = "";
|
||||
event.target.style.height = event.target.scrollHeight + "px";
|
||||
},
|
||||
importFile(el, field) {
|
||||
if (el.files.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.disabled = "disabled";
|
||||
|
||||
let r = new FileReader();
|
||||
|
||||
r.onload = () => {
|
||||
let s = el.nextSibling;
|
||||
|
||||
for (;;) {
|
||||
if (s.tagName !== "INPUT") {
|
||||
s = s.nextSibling;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
field.field.value = r.result;
|
||||
s.dispatchEvent(new Event("change"));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
el.disabled = "";
|
||||
};
|
||||
|
||||
r.readAsText(el.files[0], "utf-8");
|
||||
},
|
||||
verify(key, field, force) {
|
||||
try {
|
||||
field.message = "" + field.field.verify(field.field.value);
|
||||
field.modified = true;
|
||||
field.verified = true;
|
||||
field.error = "";
|
||||
} catch (e) {
|
||||
field.error = "";
|
||||
field.message = "";
|
||||
field.verified = false;
|
||||
|
||||
if (field.modified || force) {
|
||||
field.error = "" + e;
|
||||
}
|
||||
}
|
||||
|
||||
field.highlighted = false;
|
||||
|
||||
if (
|
||||
!field.verified &&
|
||||
(field.modified || force) &&
|
||||
field.error.length <= 0
|
||||
) {
|
||||
field.error = "Invalid";
|
||||
}
|
||||
|
||||
return field.verified;
|
||||
},
|
||||
verifyAll() {
|
||||
let verified = true;
|
||||
|
||||
for (let i = 0; i < this.current.fields.length; i++) {
|
||||
if (this.verify(i, this.current.fields[i], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
verified = false;
|
||||
}
|
||||
|
||||
return verified;
|
||||
},
|
||||
focus(key, field, focused) {
|
||||
field.highlighted = false;
|
||||
|
||||
if (!focused) {
|
||||
// Don't reset a holding field
|
||||
if (!field.inputted) {
|
||||
field.resetSuggestions(false);
|
||||
} else if (field.resetSuggestions(false)) {
|
||||
this.clickInputSuggestion(
|
||||
key,
|
||||
field,
|
||||
field.selectedSuggestionIndex(),
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.createFieldValueBackup();
|
||||
field.reloadSuggestions();
|
||||
},
|
||||
applySuggestion(key, field, suggestion) {
|
||||
this.restoreFieldValuesFromBackup(-1);
|
||||
|
||||
field.field.value = suggestion.value;
|
||||
|
||||
for (let i = 0; i < this.current.fields.length; i++) {
|
||||
this.current.fields[i].highlighted = false;
|
||||
|
||||
if (
|
||||
i === key ||
|
||||
this.current.fields[i].inputted ||
|
||||
this.current.fields[i].field.readonly
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof suggestion.fields[this.current.fields[i].field.name] ===
|
||||
"undefined"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.current.fields[i].field.value =
|
||||
suggestion.fields[this.current.fields[i].field.name];
|
||||
|
||||
if (!this.verify(i, this.current.fields[i], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.current.fields[i].highlighted = true;
|
||||
}
|
||||
},
|
||||
applySuggestionAndVerify(key, field, force, suggestion) {
|
||||
field.inputted = true;
|
||||
|
||||
this.applySuggestion(key, field, suggestion);
|
||||
|
||||
return this.verify(key, field, force);
|
||||
},
|
||||
changed(key, field, force) {
|
||||
this.createFieldValueBackup();
|
||||
|
||||
field.highlighted = false;
|
||||
field.inputted = true;
|
||||
field.enableInputSuggestionsOnAllInput();
|
||||
field.reloadSuggestions();
|
||||
|
||||
this.verify(key, field, force);
|
||||
},
|
||||
triggerSuggestions(event, key, field) {
|
||||
switch (event.key) {
|
||||
case "ArrowUp":
|
||||
event.preventDefault();
|
||||
field.moveSuggestionsCursor(true);
|
||||
|
||||
this.applySuggestionAndVerify(
|
||||
key,
|
||||
field,
|
||||
true,
|
||||
field.curentSuggestion(),
|
||||
);
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
event.preventDefault();
|
||||
field.moveSuggestionsCursor(false);
|
||||
|
||||
this.applySuggestionAndVerify(
|
||||
key,
|
||||
field,
|
||||
true,
|
||||
field.curentSuggestion(),
|
||||
);
|
||||
break;
|
||||
|
||||
case "Escape":
|
||||
if (!field.suggestionsPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.restoreFieldValuesFromBackup(key);
|
||||
this.clearFieldValueBackup();
|
||||
this.clearFieldHighlights();
|
||||
this.verify(key, field, true);
|
||||
|
||||
field.disableSuggestionsForInput(field.field.value);
|
||||
field.resetSuggestions(true);
|
||||
break;
|
||||
|
||||
case "Enter":
|
||||
if (!field.suggestionsPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.clickInputSuggestion(
|
||||
key,
|
||||
field,
|
||||
field.selectedSuggestionIndex(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
clickInputSuggestion(key, field, index) {
|
||||
const self = this;
|
||||
|
||||
field.selectSuggestion(index);
|
||||
|
||||
if (
|
||||
self.applySuggestionAndVerify(
|
||||
key,
|
||||
field,
|
||||
true,
|
||||
field.curentSuggestion(),
|
||||
)
|
||||
) {
|
||||
field.disableSuggestionsForInput(field.field.value);
|
||||
} else {
|
||||
field.enableInputSuggestionsOnAllInput();
|
||||
}
|
||||
|
||||
field.resetSuggestions(true);
|
||||
|
||||
self.clearFieldValueBackup();
|
||||
self.delayedClearFieldHighlights(hightlightClearTimeout);
|
||||
},
|
||||
async submitAndGetNext() {
|
||||
if (this.current.submitting || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current.data === null || !this.current.submittable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.verifyAll()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.submitting = true;
|
||||
|
||||
try {
|
||||
await this.current.data.submit(this.getFieldValues());
|
||||
} catch (e) {
|
||||
this.current.submitting = false;
|
||||
|
||||
alert("Submission has failed: " + e);
|
||||
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
async cancelAndGetNext() {
|
||||
if (this.current.submitting || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current.data === null || !this.current.cancellable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.submitting = true;
|
||||
|
||||
await this.current.data.cancel();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,222 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
function getTabIndex(tabIndex, field) {
|
||||
if (field.readonly) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case "text":
|
||||
case "password":
|
||||
case "checkbox":
|
||||
case "textarea":
|
||||
case "textfile":
|
||||
case "select":
|
||||
case "radio":
|
||||
return tabIndex;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function build(tabIndex, i, field) {
|
||||
return {
|
||||
verified: false,
|
||||
modified: false,
|
||||
inputted: false,
|
||||
highlighted: false,
|
||||
error: "",
|
||||
message: "",
|
||||
field: field,
|
||||
autofocus: tabIndex === 1 && !field.readonly,
|
||||
tabIndex: getTabIndex(tabIndex, field),
|
||||
blockedSuggestionValue: "",
|
||||
blockingSuggestion: false,
|
||||
nextTabIndex() {
|
||||
let nextTabIndex = 0;
|
||||
|
||||
if (this.field.readonly) {
|
||||
nextTabIndex = this.tabIndex;
|
||||
} else {
|
||||
switch (this.field.type) {
|
||||
case "radio":
|
||||
nextTabIndex = this.tabIndex + this.field.example.split(",").length;
|
||||
break;
|
||||
|
||||
default:
|
||||
nextTabIndex = this.tabIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (tabIndex >= nextTabIndex) {
|
||||
return tabIndex;
|
||||
}
|
||||
|
||||
return nextTabIndex;
|
||||
},
|
||||
nextSubTabIndex(subIndex) {
|
||||
if (this.field.readonly) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.tabIndex + subIndex;
|
||||
},
|
||||
suggestion: {
|
||||
selected: -1,
|
||||
suggestions: [],
|
||||
orignalValue: "",
|
||||
orignalValueStored: false,
|
||||
holding: false,
|
||||
needsReset: false,
|
||||
reset() {
|
||||
this.selected = -1;
|
||||
this.suggestions = [];
|
||||
this.holding = false;
|
||||
this.needsReset = false;
|
||||
this.clearStored();
|
||||
|
||||
return true;
|
||||
},
|
||||
softReset() {
|
||||
if (this.holding) {
|
||||
this.needsReset = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.reset();
|
||||
},
|
||||
hold(toHold) {
|
||||
this.holding = toHold;
|
||||
|
||||
if (this.holding || !this.needsReset) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
},
|
||||
storeOrignal(val) {
|
||||
if (this.orignalValueStored) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.orignalValue = val;
|
||||
this.orignalValueStored = true;
|
||||
},
|
||||
loadStored(defaultValue) {
|
||||
return this.orignalValueStored ? this.orignalValue : defaultValue;
|
||||
},
|
||||
clearStored() {
|
||||
this.orignalValue = "";
|
||||
this.orignalValueStored = false;
|
||||
},
|
||||
select(index, fieldValue) {
|
||||
if (this.selected < 0) {
|
||||
this.storeOrignal(fieldValue);
|
||||
}
|
||||
|
||||
if (index < -1 || index >= this.suggestions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selected = index;
|
||||
},
|
||||
cursorUp(fieldValue) {
|
||||
this.select(this.selected - 1, fieldValue);
|
||||
},
|
||||
cursorDown(fieldValue) {
|
||||
this.select(this.selected + 1, fieldValue);
|
||||
},
|
||||
cursorMove(toUp, fieldValue) {
|
||||
toUp ? this.cursorUp(fieldValue) : this.cursorDown(fieldValue);
|
||||
},
|
||||
reload(fieldValue, suggestions) {
|
||||
this.selected = -1;
|
||||
this.suggestions = [];
|
||||
|
||||
this.clearStored();
|
||||
|
||||
if (suggestions.length === 1 && suggestions[0].value === fieldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let v in suggestions) {
|
||||
this.suggestions.push({
|
||||
title: suggestions[v].title,
|
||||
value: suggestions[v].value,
|
||||
fields: suggestions[v].meta,
|
||||
});
|
||||
}
|
||||
},
|
||||
current(defaultValue) {
|
||||
if (this.selected < 0) {
|
||||
return {
|
||||
title: "Input",
|
||||
value: this.loadStored(defaultValue),
|
||||
fields: {},
|
||||
};
|
||||
}
|
||||
|
||||
return this.suggestions[this.selected];
|
||||
},
|
||||
},
|
||||
disableSuggestionsForInput(val) {
|
||||
this.blockedSuggestionValue = val;
|
||||
this.blockingSuggestion = true;
|
||||
},
|
||||
enableInputSuggestionsOnAllInput() {
|
||||
this.blockedSuggestionValue = "";
|
||||
this.blockingSuggestion = false;
|
||||
},
|
||||
suggestionsPending() {
|
||||
return this.suggestion.suggestions.length > 0;
|
||||
},
|
||||
reloadSuggestions() {
|
||||
if (
|
||||
this.blockingSuggestion &&
|
||||
this.field.value === this.blockedSuggestionValue
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.suggestion.reload(
|
||||
this.field.value,
|
||||
this.field.suggestions(this.field.value),
|
||||
);
|
||||
},
|
||||
resetSuggestions(force) {
|
||||
return force ? this.suggestion.reset() : this.suggestion.softReset();
|
||||
},
|
||||
holdSuggestions(toHold) {
|
||||
this.suggestion.hold(toHold);
|
||||
},
|
||||
moveSuggestionsCursor(toUp) {
|
||||
this.suggestion.cursorMove(toUp, this.field.value);
|
||||
},
|
||||
selectSuggestion(index) {
|
||||
this.suggestion.select(index, this.field.value);
|
||||
},
|
||||
curentSuggestion() {
|
||||
return this.suggestion.current(this.field.value);
|
||||
},
|
||||
selectedSuggestionIndex() {
|
||||
return this.suggestion.selected;
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
@import "~hack-font/build/web/hack.css";
|
||||
@import "~@azurity/pure-nerd-font/pure-nerd-font.css";
|
||||
|
||||
#connector-resource-preload-control-console {
|
||||
font-family: PureNerdFont, Hack;
|
||||
}
|
||||
#connector-resource-preload-control-console::after {
|
||||
content: " ";
|
||||
font-family: PureNerdFont, Hack;
|
||||
font-weight: bold;
|
||||
}
|
||||
#connector-resource-preload-control-console::before {
|
||||
content: " ";
|
||||
font-family: PureNerdFont, Hack;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-screen > .screen-console {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-screen > .screen-console > .console-toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 5px #0006;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group.console-toolbar-group-left {
|
||||
border-right: 1px solid #0001;
|
||||
margin-right: -1px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group.console-toolbar-group-main {
|
||||
border-left: 1px solid #0001;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item {
|
||||
padding: 15px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-title {
|
||||
font-size: 0.7em;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 5px 10px;
|
||||
color: #fff9;
|
||||
text-shadow: 1px 1px 1px #0005;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-item {
|
||||
display: block;
|
||||
font-size: 0.7em;
|
||||
padding: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-item:active {
|
||||
background: #fff3;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-item
|
||||
> .tb-key-icon {
|
||||
margin: 0 5px;
|
||||
background: #fff2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-item
|
||||
> .tb-key-icon.tb-key-resize-icon {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-item
|
||||
> .tb-key-icon:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-item
|
||||
> .tb-key-icon:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-toolbar
|
||||
> .console-toolbar-group
|
||||
> .console-toolbar-item
|
||||
.tb-item:active
|
||||
.tb-key-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-screen > .screen-console > .console-console {
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-console
|
||||
> .console-loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-console
|
||||
> .console-loading
|
||||
> .console-loading-frame {
|
||||
text-align: center;
|
||||
font-size: 1em;
|
||||
font-weight: lighter;
|
||||
padding: 20px;
|
||||
margin: 10px auto;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
#home-content
|
||||
> .screen
|
||||
> .screen-screen
|
||||
> .screen-console
|
||||
> .console-console
|
||||
> .console-loading
|
||||
> .console-loading-frame
|
||||
> .console-loading-icon {
|
||||
background: url(./busy.svg) 50% no-repeat;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
@ -1,591 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="screen-console">
|
||||
<div
|
||||
class="console-console"
|
||||
:style="'font-family: ' + typefaces + ', inherit'"
|
||||
>
|
||||
<h2 style="display: none">Console</h2>
|
||||
|
||||
<div class="console-loading">
|
||||
<div class="console-loading-frame">
|
||||
<div class="console-loading-icon"></div>
|
||||
<div class="console-loading-message">Initializing console ...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Tell you this: the background transparent below is probably the most
|
||||
important transparent setting in the entire application. Make sure user
|
||||
can see through it so they can operate the console while keep the toolbar
|
||||
open.
|
||||
-->
|
||||
<div
|
||||
v-if="toolbar"
|
||||
class="console-toolbar"
|
||||
:style="'background-color: ' + control.activeColor() + 'ee'"
|
||||
>
|
||||
<h2 style="display: none">Tool bar</h2>
|
||||
|
||||
<div class="console-toolbar-group console-toolbar-group-left">
|
||||
<div class="console-toolbar-item">
|
||||
<h3 class="tb-title">Text size</h3>
|
||||
|
||||
<ul class="lst-nostyle">
|
||||
<li>
|
||||
<a class="tb-item" href="javascript:;" @click="fontSizeUp">
|
||||
<span
|
||||
class="tb-key-icon tb-key-resize-icon icon icon-keyboardkey1 icon-iconed-bottom1"
|
||||
>
|
||||
<i>+</i>
|
||||
Increase
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="tb-item" href="javascript:;" @click="fontSizeDown">
|
||||
<span
|
||||
class="tb-key-icon tb-key-resize-icon icon icon-keyboardkey1 icon-iconed-bottom1"
|
||||
>
|
||||
<i>-</i>
|
||||
Decrease
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="console-toolbar-group console-toolbar-group-main">
|
||||
<div
|
||||
v-for="(keyType, keyTypeIdx) in screenKeys"
|
||||
:key="keyTypeIdx"
|
||||
class="console-toolbar-item"
|
||||
>
|
||||
<h3 class="tb-title">{{ keyType.title }}</h3>
|
||||
|
||||
<ul class="hlst lst-nostyle">
|
||||
<li v-for="(key, keyIdx) in keyType.keys" :key="keyIdx">
|
||||
<a
|
||||
class="tb-item"
|
||||
href="javascript:;"
|
||||
@click="sendSpecialKey(key[1])"
|
||||
v-html="$options.filters.specialKeyHTML(key[0])"
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FontFaceObserver from "fontfaceobserver";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11';
|
||||
import { WebglAddon } from "@xterm/addon-webgl";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { isNumber } from "../commands/common.js";
|
||||
import { consoleScreenKeys } from "./screen_console_keys.js";
|
||||
|
||||
import "./screen_console.css";
|
||||
import "xterm/css/xterm.css";
|
||||
|
||||
const termTypeFaces = "PureNerdFont, Hack";
|
||||
const termFallbackTypeFace = '"Cascadia Code" , monospace';
|
||||
const termTypeFaceLoadTimeout = 3000;
|
||||
const termTypeFaceLoadError =
|
||||
"Remote font " +
|
||||
termTypeFaces +
|
||||
" is unavailable, using " +
|
||||
termFallbackTypeFace +
|
||||
" instead until the remote font is loaded";
|
||||
const termDefaultFontSize = 16;
|
||||
const termMinFontSize = 8;
|
||||
const termMaxFontSize = 36;
|
||||
|
||||
function webglSupported() {
|
||||
try {
|
||||
if (typeof window !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (typeof window.WebGLRenderingContext !== "function") {
|
||||
return false;
|
||||
}
|
||||
if (typeof window.WebGL2RenderingContext !== "function") {
|
||||
return false;
|
||||
}
|
||||
return document.createElement('canvas').getContext('webgl') &&
|
||||
document.createElement('canvas').getContext('webgl2');
|
||||
} catch(e) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class Term {
|
||||
constructor(control) {
|
||||
const resizeDelayInterval = 500;
|
||||
|
||||
this.control = control;
|
||||
this.closed = false;
|
||||
this.fontSize = termDefaultFontSize;
|
||||
this.term = new Terminal({
|
||||
allowProposedApi: true,
|
||||
allowTransparency: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
fontFamily: termTypeFaces + ", " + termFallbackTypeFace,
|
||||
fontSize: this.fontSize,
|
||||
logLevel: process.env.NODE_ENV === "development" ? "info" : "off",
|
||||
theme: {
|
||||
background: this.control.activeColor(),
|
||||
},
|
||||
});
|
||||
this.fit = new FitAddon();
|
||||
|
||||
this.term.onData((data) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.control.send(data);
|
||||
});
|
||||
|
||||
this.term.onBinary((data) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.control.sendBinary(data);
|
||||
});
|
||||
|
||||
this.term.onKey((ev) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
if (!this.control.echo()) {
|
||||
return;
|
||||
}
|
||||
const printable =
|
||||
!ev.domEvent.altKey &&
|
||||
!ev.domEvent.altGraphKey &&
|
||||
!ev.domEvent.ctrlKey &&
|
||||
!ev.domEvent.metaKey;
|
||||
switch (ev.domEvent.key) {
|
||||
case "Enter":
|
||||
ev.domEvent.preventDefault();
|
||||
this.writeStr("\r\n");
|
||||
break;
|
||||
case "Backspace":
|
||||
ev.domEvent.preventDefault();
|
||||
this.writeStr("\b \b");
|
||||
break;
|
||||
default:
|
||||
if (printable) {
|
||||
ev.domEvent.preventDefault();
|
||||
this.writeStr(ev.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let resizeDelay = null,
|
||||
oldRows = 0,
|
||||
oldCols = 0;
|
||||
|
||||
this.term.onResize((dim) => {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
if (dim.cols === oldCols && dim.rows === oldRows) {
|
||||
return;
|
||||
}
|
||||
oldRows = dim.rows;
|
||||
oldCols = dim.cols;
|
||||
if (resizeDelay !== null) {
|
||||
clearTimeout(resizeDelay);
|
||||
resizeDelay = null;
|
||||
}
|
||||
resizeDelay = setTimeout(() => {
|
||||
resizeDelay = null;
|
||||
if (!isNumber(dim.cols) || !isNumber(dim.rows)) {
|
||||
return;
|
||||
}
|
||||
if (!dim.cols || !dim.rows) {
|
||||
return;
|
||||
}
|
||||
this.control.resize({
|
||||
rows: dim.rows,
|
||||
cols: dim.cols,
|
||||
});
|
||||
}, resizeDelayInterval);
|
||||
});
|
||||
}
|
||||
|
||||
init(root) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.term.open(root);
|
||||
this.term.loadAddon(this.fit);
|
||||
this.term.loadAddon(new WebLinksAddon());
|
||||
this.term.loadAddon(new Unicode11Addon());
|
||||
try {
|
||||
if (webglSupported()) {
|
||||
this.term.loadAddon(new WebglAddon());
|
||||
}
|
||||
} catch(e) {}
|
||||
this.term.unicode.activeVersion = '11';
|
||||
this.refit();
|
||||
}
|
||||
|
||||
dispatch(event) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.term.textarea.dispatchEvent(event);
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
writeStr(d) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.term.write(d);
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
setFont(value) {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.term.options.fontFamily = value;
|
||||
this.refit();
|
||||
}
|
||||
|
||||
fontSizeUp() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
if (this.fontSize >= termMaxFontSize) {
|
||||
return;
|
||||
}
|
||||
this.fontSize += 2;
|
||||
this.term.options.fontSize = this.fontSize;
|
||||
this.refit();
|
||||
}
|
||||
|
||||
fontSizeDown() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
if (this.fontSize <= termMinFontSize) {
|
||||
return;
|
||||
}
|
||||
this.fontSize -= 2;
|
||||
this.term.options.fontSize = this.fontSize;
|
||||
this.refit();
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.term.focus();
|
||||
this.refit();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
blur() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.term.blur();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
refit() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.fit.fit();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
destroyed() {
|
||||
return this.closed;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
this.closed = true;
|
||||
try {
|
||||
this.term.dispose();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// So it turns out, display: none + xterm.js == trouble, so I changed this
|
||||
// to a visibility + position: absolute appoarch. Problem resolved, and I
|
||||
// like to keep it that way.
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
specialKeyHTML(key) {
|
||||
const head = '<span class="tb-key-icon icon icon-keyboardkey1">',
|
||||
tail = "</span>";
|
||||
|
||||
return head + key.split("+").join(tail + "+" + head) + tail;
|
||||
},
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
control: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
change: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
toolbar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
viewPort: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
screenKeys: consoleScreenKeys,
|
||||
term: new Term(this.control),
|
||||
typefaces: termTypeFaces,
|
||||
runner: null,
|
||||
eventHandlers: {
|
||||
keydown: null,
|
||||
keyup: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
active(newVal, oldVal) {
|
||||
this.triggerActive(newVal);
|
||||
},
|
||||
change: {
|
||||
handler() {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fit();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
viewPort: {
|
||||
handler() {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fit();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.init();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.deinit();
|
||||
},
|
||||
methods: {
|
||||
loadRemoteFont(typefaces, timeout) {
|
||||
const tfs = typefaces.split(",");
|
||||
let observers = [];
|
||||
for (let v in tfs) {
|
||||
observers.push(new FontFaceObserver(tfs[v].trim()).load(null, timeout));
|
||||
observers.push(
|
||||
new FontFaceObserver(tfs[v].trim(), {
|
||||
weight: "bold",
|
||||
}).load(null, timeout),
|
||||
);
|
||||
}
|
||||
return Promise.all(observers);
|
||||
},
|
||||
async retryLoadRemoteFont(typefaces, timeout, onSuccess) {
|
||||
const self = this;
|
||||
for (;;) {
|
||||
try {
|
||||
onSuccess(await self.loadRemoteFont(typefaces, timeout));
|
||||
return;
|
||||
} catch (e) {
|
||||
// Retry
|
||||
}
|
||||
}
|
||||
},
|
||||
async openTerm(root, callbacks) {
|
||||
const self = this;
|
||||
try {
|
||||
await self.loadRemoteFont(termTypeFaces, termTypeFaceLoadTimeout);
|
||||
if (self.term.destroyed()) {
|
||||
return;
|
||||
}
|
||||
root.innerHTML = "";
|
||||
self.term.init(root);
|
||||
return;
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
if (self.term.destroyed()) {
|
||||
return;
|
||||
}
|
||||
root.innerHTML = "";
|
||||
callbacks.warn(termTypeFaceLoadError, false);
|
||||
self.term.setFont(termFallbackTypeFace);
|
||||
self.term.init(root);
|
||||
self.retryLoadRemoteFont(termTypeFaces, termTypeFaceLoadTimeout, () => {
|
||||
if (self.term.destroyed()) {
|
||||
return;
|
||||
}
|
||||
self.term.setFont(termTypeFaces);
|
||||
callbacks.warn(termTypeFaceLoadError, true);
|
||||
});
|
||||
},
|
||||
triggerActive(active) {
|
||||
active ? this.activate() : this.deactivate();
|
||||
},
|
||||
async init() {
|
||||
let self = this;
|
||||
|
||||
await self.openTerm(
|
||||
self.$el.getElementsByClassName("console-console")[0],
|
||||
{
|
||||
warn(msg, toDismiss) {
|
||||
self.$emit("warning", {
|
||||
text: msg,
|
||||
toDismiss: toDismiss,
|
||||
});
|
||||
},
|
||||
info(msg, toDismiss) {
|
||||
self.$emit("info", {
|
||||
text: msg,
|
||||
toDismiss: toDismiss,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (self.term.destroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.triggerActive(this.active);
|
||||
self.runRunner();
|
||||
},
|
||||
async deinit() {
|
||||
await this.closeRunner();
|
||||
await this.deactivate();
|
||||
this.term.destroy();
|
||||
},
|
||||
fit() {
|
||||
this.term.refit();
|
||||
},
|
||||
activate() {
|
||||
this.term.focus();
|
||||
this.fit();
|
||||
},
|
||||
async deactivate() {
|
||||
this.term.blur();
|
||||
},
|
||||
runRunner() {
|
||||
if (this.runner !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
this.runner = (async () => {
|
||||
try {
|
||||
for (;;) {
|
||||
if (self.term.destroyed()) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.term.writeStr(await this.control.receive());
|
||||
|
||||
self.$emit("updated");
|
||||
}
|
||||
} catch (e) {
|
||||
self.$emit("stopped", e);
|
||||
}
|
||||
})();
|
||||
},
|
||||
async closeRunner() {
|
||||
if (this.runner === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let runner = this.runner;
|
||||
this.runner = null;
|
||||
|
||||
await runner;
|
||||
},
|
||||
sendSpecialKey(key) {
|
||||
if (!this.term) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.term.dispatch(new KeyboardEvent("keydown", key));
|
||||
this.term.dispatch(new KeyboardEvent("keyup", key));
|
||||
},
|
||||
fontSizeUp() {
|
||||
this.term.fontSizeUp();
|
||||
},
|
||||
fontSizeDown() {
|
||||
this.term.fontSizeDown();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,677 +0,0 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Generated by:
|
||||
//
|
||||
// <!doctype html>
|
||||
// <html>
|
||||
// <meta charset="UTF-8" />
|
||||
// <title>KEYBOARDEVENT KEY DUMP</title>
|
||||
|
||||
// <input name="key" id="input" />
|
||||
|
||||
// <script>
|
||||
// var keyHistory = [];
|
||||
// var targetProps = [
|
||||
// "altKey",
|
||||
// "char",
|
||||
// "charCode",
|
||||
// "code",
|
||||
// "ctrlKey",
|
||||
// "key",
|
||||
// "keyCode",
|
||||
// "location",
|
||||
// "metaKey",
|
||||
// "repeat",
|
||||
// "shiftKey",
|
||||
// "which",
|
||||
// ]
|
||||
|
||||
// document.getElementById("input").addEventListener("keydown", function(e) {
|
||||
// e.preventDefault();
|
||||
|
||||
// var ev = {};
|
||||
|
||||
// for (var i in targetProps) {
|
||||
// ev[targetProps[i]] = e[targetProps[i]];
|
||||
// }
|
||||
|
||||
// keyHistory.push([e.key, ev])
|
||||
|
||||
// document.getElementById("result").innerHTML = JSON.stringify(keyHistory);
|
||||
// })
|
||||
|
||||
// </script>
|
||||
|
||||
// <div id="result">
|
||||
// </div>
|
||||
// </html>
|
||||
|
||||
export const consoleScreenKeys = [
|
||||
{
|
||||
title: "Function Keys",
|
||||
keys: [
|
||||
[
|
||||
"F1",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F1",
|
||||
ctrlKey: false,
|
||||
key: "F1",
|
||||
keyCode: 112,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 112,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F2",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F2",
|
||||
ctrlKey: false,
|
||||
key: "F2",
|
||||
keyCode: 113,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 113,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F3",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F3",
|
||||
ctrlKey: false,
|
||||
key: "F3",
|
||||
keyCode: 114,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 114,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F4",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F4",
|
||||
ctrlKey: false,
|
||||
key: "F4",
|
||||
keyCode: 115,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 115,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F5",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F5",
|
||||
ctrlKey: false,
|
||||
key: "F5",
|
||||
keyCode: 116,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 116,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F6",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F6",
|
||||
ctrlKey: false,
|
||||
key: "F6",
|
||||
keyCode: 117,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 117,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F7",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F7",
|
||||
ctrlKey: false,
|
||||
key: "F7",
|
||||
keyCode: 118,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 118,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F8",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F8",
|
||||
ctrlKey: false,
|
||||
key: "F8",
|
||||
keyCode: 119,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 119,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F9",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F9",
|
||||
ctrlKey: false,
|
||||
key: "F9",
|
||||
keyCode: 120,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 120,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F10",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F10",
|
||||
ctrlKey: false,
|
||||
key: "F10",
|
||||
keyCode: 121,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 121,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F11",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F11",
|
||||
ctrlKey: false,
|
||||
key: "F11",
|
||||
keyCode: 122,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 122,
|
||||
},
|
||||
],
|
||||
[
|
||||
"F12",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "F12",
|
||||
ctrlKey: false,
|
||||
key: "F12",
|
||||
keyCode: 123,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 123,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Misc Keys",
|
||||
keys: [
|
||||
[
|
||||
"Escape",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "Escape",
|
||||
ctrlKey: false,
|
||||
key: "Escape",
|
||||
keyCode: 27,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 27,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Tab",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "Tab",
|
||||
ctrlKey: false,
|
||||
key: "Tab",
|
||||
keyCode: 9,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 9,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Insert",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "Insert",
|
||||
ctrlKey: false,
|
||||
key: "Insert",
|
||||
keyCode: 45,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 45,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Delete",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "Delete",
|
||||
ctrlKey: false,
|
||||
key: "Delete",
|
||||
keyCode: 46,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 46,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Navigation Keys",
|
||||
keys: [
|
||||
[
|
||||
"Home",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "Home",
|
||||
ctrlKey: false,
|
||||
key: "Home",
|
||||
keyCode: 36,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 36,
|
||||
},
|
||||
],
|
||||
[
|
||||
"End",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "End",
|
||||
ctrlKey: false,
|
||||
key: "End",
|
||||
keyCode: 35,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 35,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Up " + String.fromCharCode(8593),
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "ArrowUp",
|
||||
ctrlKey: false,
|
||||
key: "ArrowUp",
|
||||
keyCode: 38,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 38,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Down " + String.fromCharCode(8595),
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "ArrowDown",
|
||||
ctrlKey: false,
|
||||
key: "ArrowDown",
|
||||
keyCode: 40,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 40,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Left " + String.fromCharCode(8592),
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "ArrowLeft",
|
||||
ctrlKey: false,
|
||||
key: "ArrowLeft",
|
||||
keyCode: 37,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 37,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Right " + String.fromCharCode(8594),
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "ArrowRight",
|
||||
ctrlKey: false,
|
||||
key: "ArrowRight",
|
||||
keyCode: 39,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 39,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Page Up " + String.fromCharCode(9652),
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "PageUp",
|
||||
ctrlKey: false,
|
||||
key: "PageUp",
|
||||
keyCode: 33,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 33,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Page Down " + String.fromCharCode(9662),
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "PageDown",
|
||||
ctrlKey: false,
|
||||
key: "PageDown",
|
||||
keyCode: 34,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 34,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Control Keys",
|
||||
keys: [
|
||||
[
|
||||
"Ctrl+C",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyC",
|
||||
ctrlKey: true,
|
||||
key: "c",
|
||||
keyCode: 67,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 67,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+Z",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyZ",
|
||||
ctrlKey: true,
|
||||
key: "z",
|
||||
keyCode: 90,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 90,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+R",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyR",
|
||||
ctrlKey: true,
|
||||
key: "r",
|
||||
keyCode: 82,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 82,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+L",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyL",
|
||||
ctrlKey: true,
|
||||
key: "l",
|
||||
keyCode: 76,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 76,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+A",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyA",
|
||||
ctrlKey: true,
|
||||
key: "a",
|
||||
keyCode: 65,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 65,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+E",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyE",
|
||||
ctrlKey: true,
|
||||
key: "e",
|
||||
keyCode: 69,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 69,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+W",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyW",
|
||||
ctrlKey: true,
|
||||
key: "w",
|
||||
keyCode: 87,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 87,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+U",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyU",
|
||||
ctrlKey: true,
|
||||
key: "u",
|
||||
keyCode: 85,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 85,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+K",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyK",
|
||||
ctrlKey: true,
|
||||
key: "k",
|
||||
keyCode: 75,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 75,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+D",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyD",
|
||||
ctrlKey: true,
|
||||
key: "d",
|
||||
keyCode: 68,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 68,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+Q",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyQ",
|
||||
ctrlKey: true,
|
||||
key: "q",
|
||||
keyCode: 81,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 81,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+X",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyX",
|
||||
ctrlKey: true,
|
||||
key: "x",
|
||||
keyCode: 88,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 88,
|
||||
},
|
||||
],
|
||||
[
|
||||
"Ctrl+T",
|
||||
{
|
||||
altKey: false,
|
||||
charCode: 0,
|
||||
code: "KeyT",
|
||||
ctrlKey: true,
|
||||
key: "t",
|
||||
keyCode: 84,
|
||||
location: 0,
|
||||
metaKey: false,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
which: 84,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,75 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#home-content.active {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#home-content > .screen {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
#home-content > .screen.screen-inactive {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-error {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
background: #b44;
|
||||
color: #fff;
|
||||
font-size: 0.75em;
|
||||
flex: 0 0;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-error.screen-error-level-error {
|
||||
background: #b44;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-error.screen-error-level-warning {
|
||||
background: #b82;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-error.screen-error-level-info {
|
||||
background: #28b;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-screen {
|
||||
flex: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#home-content > .screen > .screen-screen > .screen-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -1,107 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<main :class="{ active: screens.length > 0 }">
|
||||
<slot v-if="screens.length <= 0"></slot>
|
||||
|
||||
<div
|
||||
v-for="(screenInfo, idx) in screens"
|
||||
:key="screenInfo.id"
|
||||
:class="{ 'screen-inactive': screen !== idx }"
|
||||
class="screen"
|
||||
>
|
||||
<h1 style="display: none">Main Interface</h1>
|
||||
|
||||
<div
|
||||
v-if="screenInfo.indicator.message.length > 0"
|
||||
class="screen-error"
|
||||
:class="'screen-error-level-' + screenInfo.indicator.level"
|
||||
>
|
||||
{{ screenInfo.indicator.message }}
|
||||
</div>
|
||||
|
||||
<div class="screen-screen">
|
||||
<component
|
||||
:is="getComponent(screenInfo.ui)"
|
||||
:active="screen === idx"
|
||||
:control="screenInfo.control"
|
||||
:change="screenInfo.indicator"
|
||||
:toolbar="screenInfo.toolbar"
|
||||
:view-port="viewPort"
|
||||
:style="'background-color: ' + screenInfo.control.activeColor()"
|
||||
class="screen-content"
|
||||
@stopped="stopped(idx, $event)"
|
||||
@warning="warning(idx, $event)"
|
||||
@info="info(idx, $event)"
|
||||
@updated="updated(idx)"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConsoleScreen from "./screen_console.vue";
|
||||
|
||||
import "./screens.css";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConsoleScreen,
|
||||
},
|
||||
props: {
|
||||
screen: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
screens: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
viewPort: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getComponent(ui) {
|
||||
switch (ui) {
|
||||
case "Console":
|
||||
return "ConsoleScreen";
|
||||
|
||||
default:
|
||||
throw new Error("Unknown UI: " + ui);
|
||||
}
|
||||
},
|
||||
stopped(index, stopErr) {
|
||||
this.$emit("stopped", index, stopErr);
|
||||
},
|
||||
warning(index, msg) {
|
||||
this.$emit("warning", index, msg);
|
||||
},
|
||||
info(index, msg) {
|
||||
this.$emit("info", index, msg);
|
||||
},
|
||||
updated(index) {
|
||||
this.$emit("updated", index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,221 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#conn-status {
|
||||
z-index: 999999;
|
||||
top: 40px;
|
||||
left: 96px;
|
||||
display: none;
|
||||
width: 500px;
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
#conn-status .window-frame {
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#conn-status:before {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#conn-status {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#conn-status:before {
|
||||
left: 91px;
|
||||
}
|
||||
}
|
||||
|
||||
#conn-status.display {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#conn-status h1 {
|
||||
padding: 15px 15px 10px 15px;
|
||||
background: #a56;
|
||||
}
|
||||
|
||||
#conn-status-info {
|
||||
padding: 0 15px 15px 15px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
background: #a56;
|
||||
}
|
||||
|
||||
#conn-status.green:before {
|
||||
background: #287;
|
||||
}
|
||||
|
||||
#conn-status.green h1 {
|
||||
color: #6ba;
|
||||
background: #287;
|
||||
}
|
||||
|
||||
#conn-status.green #conn-status-info {
|
||||
background: #287;
|
||||
}
|
||||
|
||||
#conn-status.yellow:before {
|
||||
background: #da0;
|
||||
}
|
||||
|
||||
#conn-status.yellow h1 {
|
||||
color: #fff;
|
||||
background: #da0;
|
||||
}
|
||||
|
||||
#conn-status.yellow #conn-status-info {
|
||||
background: #da0;
|
||||
}
|
||||
|
||||
#conn-status.orange:before {
|
||||
background: #b73;
|
||||
}
|
||||
|
||||
#conn-status.orange h1 {
|
||||
color: #fff;
|
||||
background: #b73;
|
||||
}
|
||||
|
||||
#conn-status.orange #conn-status-info {
|
||||
background: #b73;
|
||||
}
|
||||
|
||||
#conn-status.red:before {
|
||||
background: #a33;
|
||||
}
|
||||
|
||||
#conn-status.red h1 {
|
||||
color: #fff;
|
||||
background: #a33;
|
||||
}
|
||||
|
||||
#conn-status.red #conn-status-info {
|
||||
background: #a33;
|
||||
}
|
||||
|
||||
.conn-status-chart {
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter {
|
||||
width: 50%;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter .name {
|
||||
font-size: 0.8em;
|
||||
color: #777;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter .value {
|
||||
font-size: 1.5em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter .value span {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.conn-status-chart > .chart {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.conn-status-chart > .chart g {
|
||||
fill: none;
|
||||
stroke-width: 7px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.conn-status-chart > .chart .zero {
|
||||
stroke: #3a3a3a;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.conn-status-chart > .chart .expired {
|
||||
stroke: #3a3a3a;
|
||||
}
|
||||
|
||||
#conn-status-delay {
|
||||
padding: 15px 0;
|
||||
background: #292929;
|
||||
}
|
||||
|
||||
#conn-status-delay > .counters > .counter {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
#conn-status-delay-chart-background {
|
||||
--color-start: #e43989;
|
||||
--color-stop: #9a5fca;
|
||||
}
|
||||
|
||||
#conn-status-delay-chart > g {
|
||||
fill: none;
|
||||
stroke: url(#conn-status-delay-chart-background) #2a6;
|
||||
}
|
||||
|
||||
#conn-status-traffic {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-in-background {
|
||||
--color-start: #0287a8;
|
||||
--color-stop: #06e7b6;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-in > g {
|
||||
stroke: url(#conn-status-traffic-chart-in-background) #2a6;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-out-background {
|
||||
--color-start: #e46226;
|
||||
--color-stop: #da356c;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-out > g {
|
||||
stroke: url(#conn-status-traffic-chart-out-background) #2a6;
|
||||
}
|
||||
|
||||
#conn-status-close {
|
||||
/* ID mainly use for document.getElementById */
|
||||
cursor: pointer;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
}
|
||||
@ -1,251 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<window
|
||||
id="conn-status"
|
||||
flash-class="home-window-display"
|
||||
:display="display"
|
||||
@display="$emit('display', $event)"
|
||||
>
|
||||
<h1 class="window-title">Connection status</h1>
|
||||
|
||||
<div id="conn-status-info">
|
||||
{{ status.description }}
|
||||
</div>
|
||||
|
||||
<div id="conn-status-delay" class="conn-status-chart">
|
||||
<div class="counters">
|
||||
<div class="counter">
|
||||
<div class="name">Delay</div>
|
||||
<div
|
||||
class="value"
|
||||
v-html="$options.filters.mSecondString(status.delay)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart">
|
||||
<chart
|
||||
id="conn-status-delay-chart"
|
||||
:width="480"
|
||||
:height="50"
|
||||
type="Bar"
|
||||
:enabled="display"
|
||||
:values="status.delayHistory"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="conn-status-delay-chart-background"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
>
|
||||
<stop stop-color="var(--color-start)" offset="0%" />
|
||||
<stop stop-color="var(--color-stop)" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</chart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="conn-status-traffic" class="conn-status-chart">
|
||||
<div class="counters">
|
||||
<div class="counter">
|
||||
<div class="name">Inbound</div>
|
||||
<div
|
||||
class="value"
|
||||
v-html="$options.filters.bytePerSecondString(status.inbound)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="counter">
|
||||
<div class="name">Outbound</div>
|
||||
<div
|
||||
class="value"
|
||||
v-html="$options.filters.bytePerSecondString(status.outbound)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart">
|
||||
<chart
|
||||
id="conn-status-traffic-chart-in"
|
||||
:width="480"
|
||||
:height="25"
|
||||
type="Bar"
|
||||
:max="inoutBoundMax"
|
||||
:enabled="display"
|
||||
:values="status.inboundHistory"
|
||||
@max="inboundMaxColUpdated"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="conn-status-traffic-chart-in-background"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
>
|
||||
<stop stop-color="var(--color-start)" offset="0%" />
|
||||
<stop stop-color="var(--color-stop)" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</chart>
|
||||
</div>
|
||||
|
||||
<div class="chart">
|
||||
<chart
|
||||
id="conn-status-traffic-chart-out"
|
||||
:width="480"
|
||||
:height="25"
|
||||
type="UpsideDownBar"
|
||||
:max="inoutBoundMax"
|
||||
:enabled="display"
|
||||
:values="status.outboundHistory"
|
||||
@max="outboundMaxColUpdated"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="conn-status-traffic-chart-out-background"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
>
|
||||
<stop stop-color="var(--color-start)" offset="0%" />
|
||||
<stop stop-color="var(--color-stop)" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</chart>
|
||||
</div>
|
||||
</div>
|
||||
</window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint vue/attribute-hyphenation: 0 */
|
||||
|
||||
import "./status.css";
|
||||
|
||||
import Window from "./window.vue";
|
||||
import Chart from "./chart.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
window: Window,
|
||||
chart: Chart,
|
||||
},
|
||||
filters: {
|
||||
bytePerSecondString(n) {
|
||||
const bNames = ["byte/s", "kib/s", "mib/s", "gib/s", "tib/s"];
|
||||
let remain = n,
|
||||
nUnit = bNames[0];
|
||||
|
||||
for (let i in bNames) {
|
||||
nUnit = bNames[i];
|
||||
|
||||
if (remain < 1024) {
|
||||
break;
|
||||
}
|
||||
|
||||
remain /= 1024;
|
||||
}
|
||||
|
||||
return (
|
||||
Number(remain.toFixed(2)).toLocaleString() +
|
||||
" <span>" +
|
||||
nUnit +
|
||||
"</span>"
|
||||
);
|
||||
},
|
||||
mSecondString(n) {
|
||||
if (n < 0) {
|
||||
return "??";
|
||||
}
|
||||
|
||||
const bNames = ["ms", "s", "m"];
|
||||
let remain = n,
|
||||
nUnit = bNames[0];
|
||||
|
||||
for (let i in bNames) {
|
||||
nUnit = bNames[i];
|
||||
|
||||
if (remain < 1000) {
|
||||
break;
|
||||
}
|
||||
|
||||
remain /= 1000;
|
||||
}
|
||||
|
||||
return (
|
||||
Number(remain.toFixed(2)).toLocaleString() +
|
||||
" <span>" +
|
||||
nUnit +
|
||||
"</span>"
|
||||
);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
status: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
description: "",
|
||||
delay: 0,
|
||||
delayHistory: [],
|
||||
inbound: 0,
|
||||
inboundHistory: [],
|
||||
outbound: 0,
|
||||
outboundHistory: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inoutBoundMax: 0,
|
||||
inboundMax: 0,
|
||||
outboundMax: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
inboundMaxColUpdated(d) {
|
||||
this.inboundMax = d;
|
||||
|
||||
this.inoutBoundMax =
|
||||
this.inboundMax > this.outboundMax ? this.inboundMax : this.outboundMax;
|
||||
},
|
||||
outboundMaxColUpdated(d) {
|
||||
this.outboundMax = d;
|
||||
|
||||
this.inoutBoundMax =
|
||||
this.inboundMax > this.outboundMax ? this.inboundMax : this.outboundMax;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,116 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul :id="id" :class="tabsClass">
|
||||
<li
|
||||
v-for="(tabInfo, idx) in tabs"
|
||||
:key="tabInfo.id"
|
||||
:class="{
|
||||
active: tab === idx,
|
||||
error:
|
||||
tabInfo.indicator.message.length > 0 &&
|
||||
tabInfo.indicator.level === 'error',
|
||||
updated: tabInfo.indicator.updated && tab !== idx,
|
||||
}"
|
||||
:style="
|
||||
'background: ' +
|
||||
(tab === idx ? tabInfo.control.activeColor() : tabInfo.control.color())
|
||||
"
|
||||
@click.self="switchTab(idx)"
|
||||
>
|
||||
<span class="title" :title="tabInfo.name" @click="switchTab(idx)">
|
||||
<span
|
||||
class="type"
|
||||
:title="tabInfo.info.name()"
|
||||
:style="'background: ' + tabInfo.info.color()"
|
||||
>
|
||||
{{ tabInfo.info.name()[0] }}
|
||||
</span>
|
||||
{{ tabInfo.name }}
|
||||
</span>
|
||||
|
||||
<span class="icon icon-close icon-close1" @click="closeAt(idx)"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
tab: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tabsClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
tab(newVal) {
|
||||
this.switchTabTo(newVal);
|
||||
},
|
||||
tabs(newVal) {
|
||||
if (newVal.length > this.tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.switchTabTo(newVal.length - 1);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
switchTabTo(index) {
|
||||
if (index < 0 || index >= this.tabs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tab == index) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("current", index);
|
||||
},
|
||||
switchTab(index) {
|
||||
if (index < 0 || index >= this.tabs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tab === index) {
|
||||
this.$emit("retap", index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return this.switchTabTo(index);
|
||||
},
|
||||
closeAt(index) {
|
||||
this.$emit("close", index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,148 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#tab-window {
|
||||
z-index: 999999;
|
||||
top: 40px;
|
||||
right: 0px;
|
||||
display: none;
|
||||
width: 400px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
#tab-window .window-frame {
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#tab-window:before {
|
||||
right: 19px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#tab-window {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
#tab-window.display {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tab-window-close {
|
||||
cursor: pointer;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#tab-window h1 {
|
||||
padding: 15px 15px 0 15px;
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#tab-window-list > li > .lst-wrap {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tab-window-list > li {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#tab-window-tabs {
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
opacity: 0.5;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
transition: all 0.1s linear;
|
||||
transition-property: width, top, bottom;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.active::after {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.updated::after {
|
||||
background: #fff3;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.error::after {
|
||||
background: #d55;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li > span.title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li > span.title > span.type {
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
margin-right: 3px;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
background: #222;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li > .icon-close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
margin-top: -5px;
|
||||
color: #fff6;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.active {
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.active > span.title {
|
||||
padding-right: 20px;
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<window
|
||||
id="tab-window"
|
||||
flash-class="home-window-display"
|
||||
:display="display"
|
||||
@display="$emit('display', $event)"
|
||||
>
|
||||
<h1 class="window-title">Opened tabs</h1>
|
||||
|
||||
<tab-list
|
||||
id="tab-window-tabs"
|
||||
:tab="tab"
|
||||
:tabs="tabs"
|
||||
:tabs-class="tabsClass"
|
||||
@current="$emit('current', $event)"
|
||||
@retap="$emit('retap', $event)"
|
||||
@close="$emit('close', $event)"
|
||||
></tab-list>
|
||||
</window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./tab_window.css";
|
||||
|
||||
import Window from "./window.vue";
|
||||
import TabList from "./tab_list.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
window: Window,
|
||||
"tab-list": TabList,
|
||||
},
|
||||
props: {
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tab: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tabsClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
tabs(newV) {
|
||||
if (newV.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("display", false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,77 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :id="id">
|
||||
<tab-list
|
||||
:id="id + '-tabs'"
|
||||
:tab="tab"
|
||||
:tabs="tabs"
|
||||
:tabs-class="tabsClass"
|
||||
@current="$emit('current', $event)"
|
||||
@retap="$emit('retap', $event)"
|
||||
@close="$emit('close', $event)"
|
||||
></tab-list>
|
||||
|
||||
<a
|
||||
v-if="tabs.length > 0"
|
||||
:id="id + '-list'"
|
||||
:class="listTriggerClass"
|
||||
href="javascript:;"
|
||||
@click="showList"
|
||||
></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TabList from "./tab_list.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"tab-list": TabList,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
tab: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tabsClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
listTriggerClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showList() {
|
||||
this.$emit("list", this.tabs);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -1,20 +0,0 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
@ -1,77 +0,0 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2023 Ni Rui <ranqus@gmail.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="id"
|
||||
class="window window1"
|
||||
:class="[{ display: displaying }, { [flashClass]: displaying }]"
|
||||
>
|
||||
<div class="window-frame">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<span
|
||||
:id="id + '-close'"
|
||||
class="window-close icon icon-close1"
|
||||
@click="hide"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
flashClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
displaying: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
display(newVal) {
|
||||
newVal ? this.show() : this.hide();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.displaying = true;
|
||||
|
||||
this.$emit("display", this.displaying);
|
||||
},
|
||||
hide() {
|
||||
this.displaying = false;
|
||||
|
||||
this.$emit("display", this.displaying);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user