Skip to content

Commit

Permalink
ADD: multi-server access (#1621)
Browse files Browse the repository at this point in the history
* add: box shadow to wl page

* ADD: multi server structure

* ADD: server screen and components files

* ADD: server header & logo click func

* CHANGE: styles

* FIX: i18n path

* ADD: parent & child nested components

* FIX: i18n path

* FIX: logo btn for multi servers

* REFACTOR: files path

* ADD: multi server page layouts

* ADD: saved servers & server rows

* ADD: server login form

* ADD: tooltips to server login

* ADD: server details layout

* FIX: tailwind config

* ADD: ssh layouts

* ADD: change password input

* ADD: select avatars modal

* ADD: seleted server color

* ADD: some avatars

* ADD: new avatars

* ADD: change avatar

* CHANGE: outline style

* ADD: change server password

* FIX: ssh box size

* ADD: change server password

* ADD: upload existing ssh

* FIX: machine & server name

* ADD: key delete & ssh command

* ADD: delete ssh key

* ADD: internal login functionality

* ADD: connection composable

* ADD: multi server internal login

* FIX: some backend & routing

* ADD: generate key ui & funcs

* FIX: eslint

* FIX:login

* CHANGE: multi server position & style

* FIX: remove group

* ADD: keys number into modal

* FIX: buggy logout

* FIX: switch server timing

* ADD: export logs in 150 lines or all

* REMOVE: logs

* FIX: logs current filter

* FIX: generate modal

* ADD: show/hide inputs

* CHANGE: avatar size

* CHANGE: some styles

* FIX: multi server add & remove

* FIX: login deropdown

* FIX: error

* FIX: delete ssh

* FIX: upload existing key & validation
  • Loading branch information
MaxTheGeeek authored Jan 23, 2024
1 parent 1250ff8 commit b986ad2
Show file tree
Hide file tree
Showing 72 changed files with 2,820 additions and 635 deletions.
Binary file added launcher/public/avatar/default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_11.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_12.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_13.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_14.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_15.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_17.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_18.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_19.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/avatar/server_selection_9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added launcher/public/img/icon/node-icons/all-logs.png
413 changes: 339 additions & 74 deletions launcher/public/output.css

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions launcher/src/backend/Monitoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,24 @@ export class Monitoring {
nodeConnection = this.nodeConnection;
}
if (nodeConnection.sshService.connected) {
let settings;
try {
settings = await nodeConnection.sshService.exec("ls /etc/stereum");
const settings = await nodeConnection.sshService.exec("ls /etc/stereum");

// Return true if stdout includes "stereum.yaml"
if (settings?.stdout && settings.stdout.includes("stereum.yaml")) {
return true;
}

// Return false if stderr includes "No such file or directory"
if (settings?.stderr && settings.stderr.includes("No such file or directory")) {
return false;
}
} catch (err) {
log.debug("checking stereum installation failed:", err);
}
if (settings?.stdout.includes("stereum.yaml")) return true;
}

// Default return false if none of the above conditions are met
return false;
}

Expand Down Expand Up @@ -2863,7 +2873,7 @@ rm -rf diskoutput
{
logs_since: null,
logs_until: null,
logs_tail: 150,
logs_tail: null,
logs_ts: null,
service_name: null,
},
Expand Down
1 change: 1 addition & 0 deletions launcher/src/backend/NodeConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,7 @@ export class NodeConnection {
await this.closeTunnels();
this.os = null;
this.osv = null;
console.log("logged out");
}

async restartServer() {
Expand Down
128 changes: 68 additions & 60 deletions launcher/src/backend/SSHService.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const { Client, utils: { generateKeyPairSync } } = require("ssh2");
const {
Client,
utils: { generateKeyPairSync },
} = require("ssh2");
import { createTunnel } from "./SSHServiceTunnel";
import { StringUtils } from "./StringUtils";
import { writeFile } from "fs/promises";
Expand Down Expand Up @@ -29,17 +32,17 @@ export class SSHService {
return new Promise((resolve, reject) => {
const conn = new Client();

conn.on('ready', () => {
conn.on("ready", () => {
conn.end();
resolve(true);
});

conn.on('error', (err) => {
conn.on("error", (err) => {
reject(err);
});

conn.on('banner', (msg) => {
reject(msg)
conn.on("banner", (msg) => {
reject(msg);
});

conn.connect({
Expand All @@ -49,20 +52,24 @@ export class SSHService {
password: connectionInfo.password || undefined,
privateKey: connectionInfo.privateKey || undefined,
passphrase: connectionInfo.passphrase || undefined,
readyTimeout: timeout, // Set the readyTimeout parameter
readyTimeout: timeout, // Set the readyTimeout parameter
});
});
}

async checkConnectionPool() {
let lastIndex = this.connectionPool.length - 1
const threshholdIndex = lastIndex - 2
let lastIndex = this.connectionPool.length - 1;
const threshholdIndex = lastIndex - 2;

if (this.connectionInfo && !this.addingConnection && (this.connectionPool.length < 5 || this.connectionPool[threshholdIndex]?._chanMgr?._count > 0)) {
await this.connect(this.connectionInfo)
if (
this.connectionInfo &&
!this.addingConnection &&
(this.connectionPool.length < 5 || this.connectionPool[threshholdIndex]?._chanMgr?._count > 0)
) {
await this.connect(this.connectionInfo);
}
if (this.connectionPool.length > 5 && this.connectionPool[threshholdIndex]?._chanMgr?._count === 0) {
this.removeConnectionCount++
this.removeConnectionCount++;
} else {
this.removeConnectionCount = 0;
}
Expand All @@ -73,13 +80,13 @@ export class SSHService {
}

getConnectionFromPool() {
let conn
let maxVal = 5
let conn;
let maxVal = 5;
while (!conn && maxVal < 10) {
conn = (this.connectionPool.find(c => c._chanMgr._count < maxVal))
maxVal++
conn = this.connectionPool.find((c) => c._chanMgr._count < maxVal);
maxVal++;
}
return conn
return conn;
}

async connect(connectionInfo) {
Expand All @@ -89,11 +96,10 @@ export class SSHService {
return new Promise((resolve, reject) => {
conn.on("error", (error) => {
this.addingConnection = false;
log.error(error)
log.error(error);
reject(error);
});
conn.on("close", () => {
});
conn.on("close", () => {});
//only works for ubuntu 22.04
conn.on("banner", (msg) => {
if (new RegExp(/^(?=.*\bchange\b)(?=.*\bpassword\b).*$/gm).test(msg.toLowerCase())) {
Expand All @@ -103,21 +109,22 @@ export class SSHService {
reject(msg);
}
});
conn.on("ready", async () => {
this.connectionPool.push(conn);
this.connected = true;
this.addingConnection = false;
if (this.connectionPool.length === 1) {
let test = await this.exec("ls");
if (new RegExp(/^(?=.*\bchange\b)(?=.*\bpassword\b).*$/gm).test(test.stderr.toLowerCase())) {
if (process.env.NODE_ENV === "test") {
resolve(conn);
conn
.on("ready", async () => {
this.connectionPool.push(conn);
this.connected = true;
this.addingConnection = false;
if (this.connectionPool.length === 1) {
let test = await this.exec("ls");
if (new RegExp(/^(?=.*\bchange\b)(?=.*\bpassword\b).*$/gm).test(test.stderr.toLowerCase())) {
if (process.env.NODE_ENV === "test") {
resolve(conn);
}
reject(test.stderr);
}
reject(test.stderr);
}
}
resolve(conn);
})
resolve(conn);
})
.connect({
host: connectionInfo.host,
port: parseInt(connectionInfo.port) || 22,
Expand All @@ -137,7 +144,9 @@ export class SSHService {
try {
this.connected = false;
this.connectionInfo = null;
this.connectionPool.forEach((conn) => { conn.end(); });
this.connectionPool.forEach((conn) => {
conn.end();
});
this.connectionPool = [];
resolve(true);
} catch (error) {
Expand All @@ -153,7 +162,7 @@ export class SSHService {

async execCommand(command) {
return new Promise((resolve, reject) => {
let conn = this.getConnectionFromPool()
let conn = this.getConnectionFromPool();

const data = {
rc: -1,
Expand All @@ -162,7 +171,7 @@ export class SSHService {
};
conn.exec(command, (err, stream) => {
if (err) {
log.error("ERROR:", err)
log.error("ERROR:", err);
return reject(err);
}
stream
Expand Down Expand Up @@ -252,31 +261,30 @@ export class SSHService {

async changePassword(password) {
try {
const result = await this.exec(`echo -e "${this.connectionInfo.user}:${password}" | chpasswd`)
const result = await this.exec(`echo -e "${this.connectionInfo.user}:${password}" | chpasswd`);
if (SSHService.checkExecError(result)) {
throw new Error("Failed changing password: " + SSHService.extractExecError(result));
}
this.connectionInfo.password = password
return "Password changed successfully!"
this.connectionInfo.password = password;
return "Password changed successfully!";
} catch (error) {
log.error("Failed changing password: ", error)
log.error("Failed changing password: ", error);
}
}

async generateSSHKeyPair(opts = {}) {
const path = require("path");
if (opts.pickPath.endsWith("/")) opts.pickPath = opts.pickPath.slice(0, -1, ""); //if path ends with '/' remove it
try {

//default bit values for keys
if (!opts.bits) {
switch (opts.keyType.toLowerCase()) {
case "rsa": {
opts.bits = 4096
opts.bits = 4096;
break;
}
case "ecdsa": {
opts.bits = 521
opts.bits = 521;
break;
}
case "ed25519": {
Expand All @@ -286,34 +294,33 @@ export class SSHService {
}

//Make sure opts.bits is an integer
opts.bits = parseInt(opts.bits)
opts.bits = parseInt(opts.bits);

//if passphrase is set but cipher is not, set cipher to aes256-cbc
if (opts.passphrase && !opts.cipher)
opts = { ...opts, ...{ cipher: "aes256-cbc" } }
if (opts.passphrase && !opts.cipher) opts = { ...opts, ...{ cipher: "aes256-cbc" } };

//Set SSH Key Comment
opts.comment = 'StereumSSHKey'
opts.comment = "StereumSSHKey";

//generate Keypair read exiting ones and write to file
const keyPair = generateKeyPairSync(opts.keyType, opts)
let exitingKeys = await this.readSSHKeyFile()
const keyPair = generateKeyPairSync(opts.keyType, opts);
let exitingKeys = await this.readSSHKeyFile();
if (keyPair.public) {
let allKeys = [...exitingKeys, keyPair.public]
await this.writeSSHKeyFile(allKeys)
const savePath = path.join(opts.pickPath, opts.keyType.toLowerCase())
await writeFile(savePath, keyPair.private)
await writeFile(savePath + ".pub", keyPair.public)
return allKeys
let allKeys = [...exitingKeys, keyPair.public];
await this.writeSSHKeyFile(allKeys);
const savePath = path.join(opts.pickPath, opts.keyType.toLowerCase());
await writeFile(savePath, keyPair.private);
await writeFile(savePath + ".pub", keyPair.public);
return allKeys;
}
return exitingKeys
return exitingKeys;
} catch (err) {
log.error("Failed generating key pair: ", err)
log.error("Failed generating key pair: ", err);
}
}

async readSSHKeyFile(path = `~/.ssh`) {
let authorizedKeys = []
let authorizedKeys = [];
try {
if (path.endsWith("/")) path = path.slice(0, -1, ""); //if path ends with '/' remove it
let result = await this.exec(`cat ${path}/authorized_keys`);
Expand All @@ -323,22 +330,23 @@ export class SSHService {
authorizedKeys = result.stdout.split("\n").filter((e) => e); // split in new lines and remove empty lines
} catch (err) {
log.error("Can't read authorized keys ", err);
return []
return [];
}
console.log("authorizedKeys: ", authorizedKeys);
return authorizedKeys;
}

async writeSSHKeyFile(keys = [], path = `~/.ssh`) {
try {
if (path.endsWith("/")) path = path.slice(0, -1, ""); //if path ends with '/' remove it
let newKeys = keys.join("\n")
let newKeys = keys.join("\n");
let result = await this.exec(`echo -e ${StringUtils.escapeStringForShell(newKeys)} > ${path}/authorized_keys`);
if (SSHService.checkExecError(result)) {
throw new Error("Failed writing authorized keys:\n" + SSHService.extractExecError(result));
}
} catch (err) {
log.error("Can't write authorized keys ", err);
return []
return [];
}
return keys;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<template>
<div class="col-start-5 col-end-21 row-start-1 row-span-2 grid grid-cols-6 grid-rows-2 p-1 items-end gap-2">
<div class="h-14 col-start-1 col-end-4 row-start-1 row-span-2 flex justify-center bg-[#1E2429] rounded-md p-1">
<div
class="col-start-5 col-end-21 row-start-1 row-span-2 grid grid-cols-6 grid-rows-2 p-1 items-end gap-2"
>
<div
class="h-14 col-start-1 col-end-4 row-start-1 row-span-2 flex justify-center bg-[#1E2429] rounded-md p-1"
>
<div class="w-full h-full flex justify-center items-center ml-2">
<span class="w-full text-[24px] font-bold text-gray-400 uppercase tracking-wider">
{{ t("customInstallation.customInstallationTitle") }}</span
Expand All @@ -10,7 +14,7 @@
</div>
</template>
<script setup>
import { useI18n } from "vue-i18n";
import i18n from "../../../../launcher/src/includes/i18n";

const { t } = useI18n();
const t = i18n.global.t;
</script>
Loading

0 comments on commit b986ad2

Please sign in to comment.