Added RSA support for authentication. Furthered preparations for release.
This commit is contained in:
@@ -13,21 +13,20 @@
|
|||||||
<br />
|
<br />
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/LukeGus/Termix">
|
<a href="https://github.com/LukeGus/Termix">
|
||||||
<img alt="Termimx Banner" src=./repo-images/TermixLogo.png style="width: 125px; height: auto;"> </a>
|
<img alt="Termix Banner" src=./repo-images/TermixLogo.png style="width: 125px; height: auto;"> </a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
Termix is an open-source forever free self-hosted SSH (other protocols planned, see [Planned Features](#planned-features)) server management panel inspired by [Nexterm](https://github.com/gnmyt/Nexterm). Its purpose is to provide an all-in-one docker-hosted web solution to manage your servers in one easy place. I'm using this project to help me learn [React](https://github.com/facebook/react), [Vite](https://github.com/vitejs/vite-plugin-react), and [Docker](https://www.docker.com) but also because I could never settle on a server management software that I enjoyed to use.
|
Termix is an open-source forever free self-hosted SSH (other protocols planned, see [Planned Features](#planned-features)) server management panel inspired by [Nexterm](https://github.com/gnmyt/Nexterm). Its purpose is to provide an all-in-one docker-hosted web solution to manage your servers in one easy place. I'm using this project to help me learn [React](https://github.com/facebook/react), [Vite](https://github.com/vitejs/vite-plugin-react), and [Docker](https://www.docker.com) but also because I could never settle on a server management software that I enjoyed to use.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> This app is in the VERY early stages of development. Expect bugs, data loss, and unexplainable issues! For that reason, I recommend you securely tunnel your connection through a VPN.
|
> This app is in the VERY early stages of development. Expect bugs, data loss, and unexplainable issues! For that reason, I recommend you securely tunnel your connection to Termix through a VPN.
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
- SSH (password auth only)
|
- SSH
|
||||||
- Split Screen (Up to 4) & Tab System
|
- Split Screen (Up to 4) & Tab System
|
||||||
|
|
||||||
# Planned Features
|
# Planned Features
|
||||||
- Key Auth for SSH
|
|
||||||
- VNC
|
- VNC
|
||||||
- RDP
|
- RDP
|
||||||
- SFTP (build in file transfer)
|
- SFTP (build in file transfer)
|
||||||
|
|||||||
+77
-3
@@ -1,9 +1,32 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CssVarsProvider } from '@mui/joy/styles';
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog } from '@mui/joy';
|
import { Modal, Button, FormControl, FormLabel, Input, Stack, DialogTitle, DialogContent, ModalDialog, Select, Option } from '@mui/joy';
|
||||||
import theme from './theme';
|
import theme from './theme';
|
||||||
|
|
||||||
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidden }) => {
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
if (file.name.endsWith('.rsa') || file.name.endsWith('.key') || file.name.endsWith('.pem') || file.name.endsWith('.der') || file.name.endsWith('.p8') || file.name.endsWith('.ssh')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
setForm({ ...form, rsaKey: event.target.result });
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
} else {
|
||||||
|
alert("Please upload a valid RSA private key file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = () => {
|
||||||
|
if (form.authMethod === 'Select Auth') return false;
|
||||||
|
if (!form.ip || !form.user || !form.port) return false;
|
||||||
|
if (form.authMethod === 'rsaKey' && !form.rsaKey) return false;
|
||||||
|
if (form.authMethod === 'password' && !form.password) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CssVarsProvider theme={theme}>
|
<CssVarsProvider theme={theme}>
|
||||||
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}>
|
<Modal open={!isHidden} onClose={() => setIsAddHostHidden(true)}>
|
||||||
@@ -22,7 +45,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
<form
|
<form
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleAddHost();
|
if (isFormValid()) handleAddHost();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
@@ -44,6 +67,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
value={form.ip}
|
value={form.ip}
|
||||||
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
onChange={(e) => setForm({ ...form, ip: e.target.value })}
|
||||||
required
|
required
|
||||||
|
error={!form.ip ? "Please provide an IP address" : ""}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
@@ -56,12 +80,37 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
value={form.user}
|
value={form.user}
|
||||||
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
onChange={(e) => setForm({ ...form, user: e.target.value })}
|
||||||
required
|
required
|
||||||
|
error={form.user ? "" : "Please provide a username"}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Authentication Method</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={form.authMethod}
|
||||||
|
onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })}
|
||||||
|
required
|
||||||
|
displayEmpty
|
||||||
|
error={!form.authMethod || form.authMethod === 'Select Auth'}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.general.disabled,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Option value="" disabled>
|
||||||
|
Select Auth
|
||||||
|
</Option>
|
||||||
|
<Option value="password">Password</Option>
|
||||||
|
<Option value="rsaKey">RSA Key</Option>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{form.authMethod === 'password' && (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Host Password</FormLabel>
|
<FormLabel>Host Password</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -69,12 +118,34 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||||
required
|
required
|
||||||
|
error={form.password ? "" : "Please provide a password"}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
)}
|
||||||
|
{form.authMethod === 'rsaKey' && (
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>RSA Key</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
required
|
||||||
|
error={!form.rsaKey ? "Please upload a valid RSA private key file" : ""}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.general.primary,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: 1,
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '100%',
|
||||||
|
minWidth: 'auto',
|
||||||
|
minHeight: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>Host Port</FormLabel>
|
<FormLabel>Host Port</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
@@ -92,6 +163,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={!isFormValid()}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: theme.palette.general.primary,
|
backgroundColor: theme.palette.general.primary,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
@@ -116,8 +188,10 @@ AddHostModal.propTypes = {
|
|||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
ip: PropTypes.string.isRequired,
|
ip: PropTypes.string.isRequired,
|
||||||
user: PropTypes.string.isRequired,
|
user: PropTypes.string.isRequired,
|
||||||
password: PropTypes.string.isRequired,
|
password: PropTypes.string,
|
||||||
|
rsaKey: PropTypes.string,
|
||||||
port: PropTypes.number.isRequired,
|
port: PropTypes.number.isRequired,
|
||||||
|
authMethod: PropTypes.string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
setForm: PropTypes.func.isRequired,
|
setForm: PropTypes.func.isRequired,
|
||||||
handleAddHost: PropTypes.func.isRequired,
|
handleAddHost: PropTypes.func.isRequired,
|
||||||
|
|||||||
+5
-4
@@ -80,14 +80,15 @@ function App() {
|
|||||||
}, [splitTabIds]);
|
}, [splitTabIds]);
|
||||||
|
|
||||||
const handleAddHost = () => {
|
const handleAddHost = () => {
|
||||||
if (form.ip && form.user && form.password && form.port) {
|
if (form.ip && form.user && ((form.authMethod === 'password' && form.password) || (form.authMethod === 'rsaKey' && form.rsaKey)) && form.port) {
|
||||||
const newTerminal = {
|
const newTerminal = {
|
||||||
id: nextId,
|
id: nextId,
|
||||||
title: form.name || form.ip,
|
title: form.name || form.ip,
|
||||||
hostConfig: {
|
hostConfig: {
|
||||||
ip: form.ip,
|
ip: form.ip,
|
||||||
user: form.user,
|
user: form.user,
|
||||||
password: form.password,
|
password: form.authMethod === 'password' ? form.password : undefined,
|
||||||
|
rsaKey: form.authMethod === 'rsaKey' ? form.rsaKey : undefined,
|
||||||
port: Number(form.port),
|
port: Number(form.port),
|
||||||
},
|
},
|
||||||
terminalRef: null,
|
terminalRef: null,
|
||||||
@@ -96,7 +97,7 @@ function App() {
|
|||||||
setActiveTab(nextId);
|
setActiveTab(nextId);
|
||||||
setNextId(nextId + 1);
|
setNextId(nextId + 1);
|
||||||
setIsAddHostHidden(true);
|
setIsAddHostHidden(true);
|
||||||
setForm({ name: "", ip: "", user: "", password: "", port: 22 });
|
setForm({ name: "", ip: "", user: "", password: "", rsaKey: "", port: 22, authMethod: "password" });
|
||||||
} else {
|
} else {
|
||||||
alert("Please fill out all fields.");
|
alert("Please fill out all fields.");
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ function App() {
|
|||||||
} else if (splitTabIds.length > 1) {
|
} else if (splitTabIds.length > 1) {
|
||||||
return "grid grid-cols-2 grid-rows-2 gap-4 h-full overflow-hidden";
|
return "grid grid-cols-2 grid-rows-2 gap-4 h-full overflow-hidden";
|
||||||
}
|
}
|
||||||
return "flex flex-col h-full";
|
return "flex flex-col h-full gap-4";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ io.on("connection", (socket) => {
|
|||||||
let stream = null;
|
let stream = null;
|
||||||
|
|
||||||
socket.on("connectToHost", (cols, rows, hostConfig) => {
|
socket.on("connectToHost", (cols, rows, hostConfig) => {
|
||||||
if (!hostConfig || !hostConfig.ip || !hostConfig.user || !hostConfig.password || !hostConfig.port) {
|
if (!hostConfig || !hostConfig.ip || !hostConfig.user || (!hostConfig.password && !hostConfig.rsaKey) || !hostConfig.port) {
|
||||||
console.error("Invalid hostConfig received:", hostConfig);
|
console.error("Invalid hostConfig received:", hostConfig);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Received hostConfig:", hostConfig);
|
console.log("Received hostConfig:", hostConfig);
|
||||||
const { ip, port, user, password } = hostConfig;
|
const { ip, port, user, password, rsaKey } = hostConfig;
|
||||||
|
|
||||||
const conn = new SSHClient();
|
const conn = new SSHClient();
|
||||||
conn
|
conn
|
||||||
@@ -89,6 +89,7 @@ io.on("connection", (socket) => {
|
|||||||
port: port,
|
port: port,
|
||||||
username: user,
|
username: user,
|
||||||
password: password,
|
password: password,
|
||||||
|
privateKey: rsaKey ? Buffer.from(rsaKey) : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user