This commit is contained in:
Grégory Lebreton 2024-01-22 10:40:55 +01:00
parent b9a8c13426
commit 32ff8686f4
59 changed files with 0 additions and 13756 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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&quest; Checkout " +
'<a href="http://www.telnet.org/htm/places.htm" target="blank">' +
"telnet.org</a> for public servers.",
type: "text",
value: "",
example: "telnet.vaguly.com:23",
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", () => {});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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