Added RSA support for authentication. Furthered preparations for release.

This commit is contained in:
Karmaa
2025-03-06 17:52:27 -06:00
parent 963e54bf15
commit 0d464cdf56
4 changed files with 95 additions and 20 deletions
+3 -4
View File
@@ -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)
+84 -10
View File
@@ -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,6 +80,7 @@ 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,
@@ -63,18 +88,64 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
/> />
</FormControl> </FormControl>
<FormControl> <FormControl>
<FormLabel>Host Password</FormLabel> <FormLabel>Authentication Method</FormLabel>
<Input <Select
type="password" value={form.authMethod}
value={form.password} onChange={(e, newValue) => setForm({ ...form, authMethod: newValue })}
onChange={(e) => setForm({ ...form, password: e.target.value })}
required required
displayEmpty
error={!form.authMethod || form.authMethod === 'Select Auth'}
sx={{ sx={{
backgroundColor: theme.palette.general.primary, backgroundColor: !form.authMethod || form.authMethod === 'Select Auth' ? theme.palette.general.tertiary : theme.palette.general.primary,
color: theme.palette.text.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> </FormControl>
{form.authMethod === 'password' && (
<FormControl>
<FormLabel>Host Password</FormLabel>
<Input
type="password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
required
error={form.password ? "" : "Please provide a password"}
sx={{
backgroundColor: theme.palette.general.primary,
color: theme.palette.text.primary,
}}
/>
</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
View File
@@ -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 (
+3 -2
View File
@@ -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,
}); });
}); });