Better encryption for everything, new session login, rewrote a lot of database code changing its storage methods. Prepared for release of 2.0.

This commit is contained in:
Karmaa
2025-03-15 01:42:43 -05:00
parent e2e35e6130
commit 420fae828b
12 changed files with 1159 additions and 545 deletions

View File

@@ -181,7 +181,7 @@ function App() {
const handleSaveHost = () => {
let hostConfig = {
name: addHostForm.name,
name: addHostForm.name || addHostForm.ip,
ip: addHostForm.ip,
user: addHostForm.user,
password: addHostForm.authMethod === 'password' ? addHostForm.password : undefined,
@@ -273,10 +273,10 @@ function App() {
const deleteHost = (hostConfig) => {
if (userRef.current) {
userRef.current.deleteHost({
hostConfig,
hostId: hostConfig._id,
});
}
}
};
const updateEditHostForm = (hostConfig) => {
if (hostConfig) {
@@ -289,18 +289,16 @@ function App() {
const handleEditHost = () => {
if (editHostForm.ip && editHostForm.user && ((editHostForm.authMethod === 'password' && editHostForm.password) || (editHostForm.authMethod === 'rsaKey' && editHostForm.rsaKey)) && editHostForm.port && editHostForm.authMethod !== 'Select Auth') {
const user = getUser();
editHostForm.rememberHost = true;
if (user && currentHostConfig) {
userRef.current.editExistingHost({
userId: user.id,
if (currentHostConfig) {
userRef.current.editHost({
oldHostConfig: currentHostConfig,
newHostConfig: editHostForm,
});
setIsEditHostHidden(true);
} else {
console.error("User or currentHostConfig is null");
alert("Host not found");
}
} else {
alert("Please fill out all fields.");

View File

@@ -27,13 +27,12 @@ function Launchpad({
useEffect(() => {
const handleClickOutside = (event) => {
// Close the launchpad when neither form is visible and no error is showing
if (
launchpadRef.current &&
!launchpadRef.current.contains(event.target) &&
isAddHostHidden && // Only close if addHost form is hidden
isEditHostHidden && // Only close if editHost form is hidden
isErrorHidden // Only close if error is hidden
isAddHostHidden &&
isEditHostHidden &&
isErrorHidden
) {
onClose();
}
@@ -47,8 +46,8 @@ function Launchpad({
}, [onClose, isAddHostHidden, isEditHostHidden, isErrorHidden]);
const handleEditHostClick = () => {
setIsAddHostHidden(false); // Open the form for editing
setActiveApp('hostViewer'); // Set active app to HostViewer
setIsAddHostHidden(false);
setActiveApp('hostViewer');
};
return (
@@ -174,10 +173,10 @@ function Launchpad({
connectToHost={connectToHost}
setIsAddHostHidden={setIsAddHostHidden}
deleteHost={deleteHost}
editHost={editHost} // Pass editHost here
editHost={editHost}
createFolder={createFolder}
moveHostToFolder={moveHostToFolder}
onEditHostClick={handleEditHostClick} // Pass the handler to the form
onEditHostClick={handleEditHostClick}
/>
)}
</div>

View File

@@ -63,10 +63,10 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible }, ref) => {
const socket = io(
window.location.hostname === "localhost"
? "http://localhost:8081" // Modified path here
? "http://localhost:8081"
: "/",
{
path: "/ssh.io/socket.io", // Same path, no need to modify
path: "/ssh.io/socket.io",
transports: ["websocket", "polling"],
}
);

View File

@@ -1,219 +1,237 @@
import { useRef, forwardRef, useImperativeHandle } from "react";
import { useRef, forwardRef, useImperativeHandle, useEffect } from "react";
import io from "socket.io-client";
import PropTypes from "prop-types";
let socket;
const SOCKET_URL = window.location.hostname === "localhost"
? "http://localhost:8082/database.io"
: "/database.io";
if (!socket) {
socket = io(
window.location.hostname === "localhost"
? "http://localhost:8082/database.io"
: "/database.io",
{
path: "/database.io/socket.io",
transports: ["websocket", "polling"],
}
);
}
const socket = io(SOCKET_URL, {
path: "/database.io/socket.io",
transports: ["websocket", "polling"],
autoConnect: false,
});
export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSuccess, onFailure }, ref) => {
export const User = forwardRef(({
onLoginSuccess,
onCreateSuccess,
onDeleteSuccess,
onFailure
}, ref) => {
const socketRef = useRef(socket);
const currentUser = useRef(null);
const createUser = (userConfig) => {
if (socketRef.current) {
socketRef.current.emit("createUser", {
username: userConfig.username,
password: userConfig.password,
useEffect(() => {
socketRef.current.connect();
return () => socketRef.current.disconnect();
}, []);
useEffect(() => {
const verifySession = async () => {
const storedSession = localStorage.getItem("sessionToken");
if (!storedSession || storedSession === "undefined") return;
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("verifySession", { sessionToken: storedSession }, resolve);
});
if (response?.success) {
currentUser.current = {
id: response.user.id,
username: response.user.username,
sessionToken: storedSession,
};
onLoginSuccess(response.user);
} else {
localStorage.removeItem("sessionToken");
onFailure("Session expired");
}
} catch (error) {
onFailure(error.message);
}
};
verifySession();
}, []);
const createUser = async (userConfig) => {
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("createUser", userConfig, resolve);
});
socketRef.current.once("userCreated", (data) => {
if (response?.user?.sessionToken) {
currentUser.current = {
id: data.user._id,
username: data.user.username,
sessionToken: data.user.sessionToken,
id: response.user.id,
username: response.user.username,
sessionToken: response.user.sessionToken,
};
localStorage.setItem('sessionToken', data.user.sessionToken);
onCreateSuccess(data);
});
socketRef.current.once("error", (error) => {
console.error(error);
const errorMsg = (error && typeof error === 'object' && error !== null)
? error.error || error.message || 'An error occurred'
: String(error);
onFailure(errorMsg);
});
localStorage.setItem("sessionToken", response.user.sessionToken);
onCreateSuccess(response.user);
} else {
throw new Error(response?.error || "User creation failed");
}
} catch (error) {
onFailure(error.message);
}
};
const loginUser = (userConfig) => {
if (socketRef.current) {
setTimeout(() => {
socketRef.current.emit("loginUser", {
username: userConfig.username,
password: userConfig.password,
sessionToken: userConfig.sessionToken,
});
const loginUser = async ({ username, password, sessionToken }) => {
try {
const response = await new Promise((resolve) => {
const credentials = sessionToken ? { sessionToken } : { username, password };
socketRef.current.emit("loginUser", credentials, resolve);
});
socketRef.current.once("userFound", (data) => {
currentUser.current = {
id: data._id,
username: data.username,
sessionToken: data.sessionToken,
};
localStorage.setItem('sessionToken', data.sessionToken);
onLoginSuccess(data);
});
socketRef.current.once("error", (error) => {
console.error(error);
const errorMsg = (error && typeof error === 'object' && error !== null)
? error.error || error.message || 'An error occurred'
: String(error);
onFailure(errorMsg);
});
}, 500);
if (response?.success) {
currentUser.current = {
id: response.user.id,
username: response.user.username,
sessionToken: response.user.sessionToken,
};
localStorage.setItem("sessionToken", response.user.sessionToken);
onLoginSuccess(response.user);
} else {
throw new Error(response?.error || "Login failed");
}
} catch (error) {
onFailure(error.message);
}
};
const logoutUser = () => {
localStorage.removeItem('sessionToken');
localStorage.removeItem("sessionToken");
currentUser.current = null;
onLoginSuccess(null);
};
const deleteUser = () => {
if (currentUser.current?.id && socketRef.current) {
socketRef.current.emit("deleteUser", {
userId: currentUser.current.id,
const deleteUser = async () => {
if (!currentUser.current) return onFailure("No user logged in");
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("deleteUser", {
userId: currentUser.current.id,
sessionToken: currentUser.current.sessionToken,
}, resolve);
});
socketRef.current.once("userDeleted", (data) => {
onDeleteSuccess(data);
currentUser.current = null;
localStorage.removeItem('sessionToken');
});
socketRef.current.once("error", (error) => {
console.error(error);
const errorMsg = (error && typeof error === 'object' && error !== null)
? error.error || error.message || 'An error occurred'
: String(error);
onFailure(errorMsg);
});
} else {
onFailure("No user is currently logged in.");
if (response?.success) {
logoutUser();
onDeleteSuccess(response);
} else {
throw new Error(response?.error || "User deletion failed");
}
} catch (error) {
onFailure(error.message);
}
};
const saveHost = (hostConfig) => {
if (currentUser.current?.id && socketRef.current) {
socketRef.current.emit("saveHostConfig", {
userId: currentUser.current.id,
hostConfig: hostConfig,
const saveHost = async (hostConfig) => {
if (!currentUser.current) return onFailure("Not authenticated");
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("saveHostConfig", {
userId: currentUser.current.id,
sessionToken: currentUser.current.sessionToken,
...hostConfig
}, resolve);
});
socketRef.current.once("error", (error) => {
onFailure(error);
});
} else {
onFailure("No user is currently logged in.");
if (!response?.success) {
throw new Error(response?.error || "Failed to save host");
}
} catch (error) {
onFailure(error.message);
}
}
};
const getUser = () => {
return currentUser.current;
}
const getAllHosts = async () => {
if (!currentUser.current) return [];
const getAllHosts = () => {
return new Promise((resolve, reject) => {
if (currentUser.current?.id && socketRef.current) {
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("getHosts", {
userId: currentUser.current.id,
});
sessionToken: currentUser.current.sessionToken,
}, resolve);
});
socketRef.current.once("hostsFound", (data) => {
if (data && Array.isArray(data)) {
resolve(data);
} else {
reject("Invalid data received.");
}
});
socketRef.current.once("error", (error) => {
console.error(error);
const errorMsg = (error && typeof error === 'object' && error !== null)
? error.error || error.message || 'An error occurred'
: String(error);
reject(errorMsg);
});
if (response?.success) {
return response.hosts;
} else {
reject("No user is currently logged in.");
throw new Error(response?.error || "Failed to fetch hosts");
}
});
};
const deleteHost = (hostConfig) => {
if (currentUser.current?.id && socketRef.current) {
socketRef.current.emit("deleteHost", {
userId: currentUser.current.id,
hostConfig: hostConfig,
});
socketRef.current.once("error", (error) => {
onFailure(error);
});
} else {
onFailure("No user is currently logged in.");
}
}
const editExistingHost = ({ userId, oldHostConfig, newHostConfig }) => {
if (currentUser.current?.id && socketRef.current) {
socketRef.current.emit("editHost", {
userId: userId,
oldHostConfig: oldHostConfig,
newHostConfig: newHostConfig,
});
socketRef.current.once("error", (error) => {
onFailure(error);
});
} else {
onFailure("No user is currently logged in.");
} catch (error) {
onFailure(error.message);
return [];
}
};
const createFolder = (folderName) => {
if (currentUser.current?.id && socketRef.current) {
socketRef.current.emit("createFolder", {
userId: currentUser.current.id,
folderName: folderName,
const deleteHost = async ({ hostId }) => {
if (!currentUser.current) return onFailure("Not authenticated");
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("deleteHost", {
userId: currentUser.current.id,
sessionToken: currentUser.current.sessionToken,
hostId: hostId,
}, resolve);
});
socketRef.current.once("error", (error) => {
onFailure(error);
});
} else {
onFailure("No user is currently logged in.");
if (!response?.success) {
throw new Error(response?.error || "Failed to delete host");
}
} catch (error) {
onFailure(error.message);
}
}
};
const moveHostToFolder = (folderName, hostConfig) => {
if (currentUser.current?.id && socketRef.current) {
socketRef.current.emit("moveHostToFolder", {
userId: currentUser.current.id,
folderName: folderName,
hostConfig: hostConfig,
const editHost = async ({ oldHostConfig, newHostConfig }) => {
if (!currentUser.current) return onFailure("Not authenticated");
try {
console.log('Editing host with configs:', { oldHostConfig, newHostConfig });
const response = await new Promise((resolve) => {
socketRef.current.emit("editHost", {
userId: currentUser.current.id,
sessionToken: currentUser.current.sessionToken,
oldHostConfig,
newHostConfig,
}, resolve);
});
socketRef.current.once("error", (error) => {
onFailure(error);
});
} else {
onFailure("No user is currently logged in.");
if (!response?.success) {
throw new Error(response?.error || "Failed to edit host");
}
} catch (error) {
onFailure(error.message);
}
}
};
const shareHost = async (hostId, targetUsername) => {
if (!currentUser.current) return onFailure("Not authenticated");
try {
const response = await new Promise((resolve) => {
socketRef.current.emit("shareHost", {
userId: currentUser.current.id,
sessionToken: currentUser.current.sessionToken,
hostId,
targetUsername,
}, resolve);
});
if (!response?.success) {
throw new Error(response?.error || "Failed to share host");
}
} catch (error) {
onFailure(error.message);
}
};
useImperativeHandle(ref, () => ({
createUser,
@@ -221,15 +239,14 @@ export const User = forwardRef(({ onLoginSuccess, onCreateSuccess, onDeleteSucce
logoutUser,
deleteUser,
saveHost,
getUser,
getAllHosts,
deleteHost,
editExistingHost,
createFolder,
moveHostToFolder,
shareHost,
editHost,
getUser: () => currentUser.current,
}));
return <div></div>;
return null;
});
User.displayName = "User";

View File

@@ -4,46 +4,38 @@ import { Button } from "@mui/joy";
function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, editHost }) {
const [hosts, setHosts] = useState([]);
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const isMounted = useRef(true);
const fetchHosts = async () => {
try {
const savedHosts = await getHosts();
if (isMounted.current) {
setHosts(savedHosts || []);
setIsLoading(false);
}
} catch (error) {
console.error("Host fetch failed:", error);
if (isMounted.current) {
setHosts([]);
setIsLoading(false);
}
}
};
useEffect(() => {
isMounted.current = true;
fetchHosts();
async function fetchInitialHosts() {
try {
const savedHosts = await getHosts();
if (isMounted.current) {
setHosts(savedHosts || []);
setInitialLoadComplete(true);
}
} catch (error) {
console.error("Initial host fetch failed:", error);
if (isMounted.current) {
setHosts([]);
setInitialLoadComplete(true);
}
}
}
fetchInitialHosts();
const intervalId = setInterval(async () => {
try {
const savedHosts = await getHosts();
if (isMounted.current) {
setHosts(savedHosts || []);
}
} catch (error) {
console.error("Periodic host update failed:", error);
}
const intervalId = setInterval(() => {
fetchHosts();
}, 2000);
return () => {
isMounted.current = false;
clearInterval(intervalId);
};
}, [getHosts]);
}, []);
return (
<div className="h-full w-full p-4 text-white flex flex-col">
@@ -61,25 +53,29 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
</Button>
</div>
<div className="flex-grow overflow-auto">
{hosts.length > 0 ? (
{isLoading ? (
<p className="text-gray-300">Loading hosts...</p>
) : hosts.length > 0 ? (
<div className="flex flex-col gap-2 w-full">
{hosts.map((hostWrapper, index) => {
const hostConfig = hostWrapper.hostConfig || {};
const hostConfig = hostWrapper.config || {};
if (!hostConfig) {
return null;
}
return (
<div key={index} className="flex justify-between items-center bg-neutral-800 p-3 rounded-lg shadow-md border border-neutral-700 w-full">
<div>
<p className="font-semibold">{hostConfig.name || hostConfig.ip}</p>
<p className="text-sm text-gray-400">
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : hostConfig.ip}:{hostConfig.port}
{hostConfig.user ? `${hostConfig.user}@${hostConfig.ip}` : `${hostConfig.ip}:${hostConfig.port}`}
</p>
</div>
<div className="flex gap-2">
<Button
className="text-black"
onClick={() => {
connectToHost(hostConfig);
}}
onClick={() => connectToHost(hostConfig)}
sx={{
backgroundColor: "#6e6e6e",
"&:hover": { backgroundColor: "#0f0f0f" }
@@ -90,7 +86,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
<Button
className="text-black"
onClick={() => {
deleteHost(hostConfig);
deleteHost({ ...hostConfig, _id: hostWrapper._id });
}}
sx={{
backgroundColor: "#6e6e6e",
@@ -117,7 +113,7 @@ function HostViewer({ getHosts, connectToHost, setIsAddHostHidden, deleteHost, e
})}
</div>
) : (
<p className="text-gray-300">Hosts are either loading or do not exist...</p>
<p className="text-gray-300">No hosts available...</p>
)}
</div>
</div>

View File

@@ -1,341 +1,385 @@
const http = require("http");
const socketIo = require("socket.io");
const mongoose = require("mongoose");
const http = require('http');
const socketIo = require('socket.io');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
require('dotenv').config();
const logger = {
info: (...args) => console.log(`🔧 [${new Date().toISOString()}] INFO:`, ...args),
error: (...args) => console.error(`❌ [${new Date().toISOString()}] ERROR:`, ...args),
warn: (...args) => console.warn(`⚠️ [${new Date().toISOString()}] WARN:`, ...args),
debug: (...args) => console.debug(`🔍 [${new Date().toISOString()}] DEBUG:`, ...args)
};
const server = http.createServer();
const io = socketIo(server, {
path: "/database.io/socket.io",
cors: {
origin: "*",
methods: ["GET", "POST"],
credentials: true
},
allowEIO3: true
path: '/database.io/socket.io',
cors: { origin: '*', methods: ['GET', 'POST'] }
});
const dbNamespace = io.of("/database.io");
async function connectToMongoDB() {
try {
const mongoUrl = process.env.MONGO_URL || 'mongodb://mongodb:27017/termix';
await mongoose.connect(mongoUrl, {});
console.log('Connected to MongoDB');
const db = mongoose.connection.db;
// Create the 'users' collection if it doesn't exist
const collections = await db.listCollections().toArray();
if (!collections.find(col => col.name === 'users')) {
await db.createCollection('users');
console.log('Successfully created collection: users');
}
} catch (error) {
console.error('Error connecting to MongoDB:', error);
}
}
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
sessionToken: { type: String, required: true },
sshConnections: { type: [Object], default: [] },
sessionToken: { type: String, required: true }
});
const hostSchema = new mongoose.Schema({
name: { type: String, required: true },
config: { type: String, required: true },
users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }
});
const User = mongoose.model('User', userSchema);
const Host = mongoose.model('Host', hostSchema);
async function createUser(username, password) {
const getEncryptionKey = (userId, sessionToken) => {
return crypto.scryptSync(`${userId}-${sessionToken}`, 'salt', 32);
};
const encryptData = (data, userId, sessionToken) => {
try {
const userExists = await User.findOne({ username });
if (userExists) {
return { error: "User already exists for username" };
}
const sessionToken = crypto.randomBytes(64).toString('hex');
const newUser = new User({ username, password, sessionToken });
await newUser.save();
return { success: true, user: { _id: newUser._id, username: newUser.username, sessionToken: newUser.sessionToken } };
} catch (err) {
return { error: 'Error creating user: ' + err.message };
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', getEncryptionKey(userId, sessionToken), iv);
const encrypted = Buffer.concat([cipher.update(JSON.stringify(data)), cipher.final()]);
return `${iv.toString('hex')}:${encrypted.toString('hex')}:${cipher.getAuthTag().toString('hex')}`;
} catch (error) {
logger.error('Encryption failed:', error);
return null;
}
}
};
async function loginUser(username, password) {
const decryptData = (encryptedData, userId, sessionToken) => {
try {
const user = await User.findOne({ username, password });
if (user) {
if (!user.sessionToken) {
user.sessionToken = crypto.randomBytes(64).toString('hex');
await user.save();
const [ivHex, contentHex, authTagHex] = encryptedData.split(':');
const iv = Buffer.from(ivHex, 'hex');
const content = Buffer.from(contentHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', getEncryptionKey(userId, sessionToken), iv);
decipher.setAuthTag(authTag);
return JSON.parse(Buffer.concat([decipher.update(content), decipher.final()]).toString());
} catch (error) {
logger.error('Decryption failed:', error);
return null;
}
};
mongoose.connect(process.env.MONGO_URL || 'mongodb://localhost:27017/termix')
.then(() => logger.info('Connected to MongoDB'))
.catch(err => logger.error('MongoDB connection error:', err));
io.of('/database.io').on('connection', (socket) => {
socket.on('createUser', async ({ username, password }, callback) => {
try {
logger.debug(`Creating user: ${username}`);
if (await User.exists({ username })) {
logger.warn(`Username already exists: ${username}`);
return callback({ error: 'Username already exists' });
}
return {
_id: user._id,
username: user.username,
sessionToken: user.sessionToken,
};
} else {
return { error: 'User not found or incorrect credentials for username' };
}
} catch (err) {
return { error: 'Error checking user: ' + err.message };
}
}
async function loginWithToken(sessionToken) {
try {
const user = await User.findOne({ sessionToken });
if (user) {
return {
_id: user._id,
username: user.username,
sessionToken: user.sessionToken,
};
} else {
return { error: 'Invalid session token' };
}
} catch (err) {
return { error: 'Error checking session token: ' + err.message };
}
}
async function deleteUser(userId) {
try {
const user = await User.findById(userId);
if (user) {
await User.deleteOne({ _id: userId });
return { success: true };
} else {
return { error: 'User not found' };
}
} catch (err) {
return { error: 'Error removing user: ' + err.message };
}
}
async function saveHostConfig(userId, hostConfig) {
try {
const user = await User.findById(userId);
if (user) {
user.sshConnections.push(hostConfig);
await user.save();
return { success: true };
} else {
return { error: 'User not found' };
}
} catch (err) {
return { error: 'Error saving host config: ' + err.message };
}
}
async function getHosts(userId) {
try {
const user = await User.findById(userId);
if (user) {
return user.sshConnections;
} else {
return { error: 'User not found' };
}
} catch (err) {
return { error: 'Error getting hosts: ' + err.message };
}
}
async function deleteHost(userId, hostConfig) {
try {
const user = await User.findById(userId);
if (user) {
user.sshConnections = user.sshConnections.filter(connection => {
const matches =
connection.name === hostConfig.name &&
connection.ip === hostConfig.ip &&
connection.port === hostConfig.port &&
connection.user === hostConfig.user;
return !matches;
const sessionToken = crypto.randomBytes(64).toString('hex');
const user = await User.create({
username,
password: await bcrypt.hash(password, 10),
sessionToken
});
await user.save();
return { success: true };
} else {
return { error: 'User not found' };
logger.info(`User created: ${username}`);
callback({ success: true, user: {
id: user._id,
username: user.username,
sessionToken
}});
} catch (error) {
logger.error('User creation error:', error);
callback({ error: 'User creation failed' });
}
} catch (err) {
return { error: 'Error deleting host: ' + err.message };
}
}
});
async function editHost(userId, oldHostConfig, newHostConfig) {
try {
const user = await User.findById(userId);
if (user) {
user.sshConnections = user.sshConnections.map(connection => {
const matches =
connection.hostConfig.name === oldHostConfig.name &&
connection.hostConfig.ip === oldHostConfig.ip &&
connection.hostConfig.port === oldHostConfig.port &&
connection.hostConfig.user === oldHostConfig.user;
if (matches) {
return { hostConfig: newHostConfig };
} else {
return connection;
}
});
await user.save();
return { success: true };
} else {
return { error: 'User not found' };
}
} catch (err) {
return { error: 'Error editing host: ' + err.message };
}
}
async function createFolder(userId, folderName) {
try {
const user = await User.findById(userId);
if (user) {
user.sshConnections.push({ folderName, connections: [] });
await user.save();
return { success: true };
} else {
return { error: 'User not found' };
}
} catch (err) {
return { error: 'Error creating folder: ' + err.message };
}
}
async function moveHostToFolder(userId, hostConfig, folderName) {
try {
const user = await User.findById(userId);
if (user) {
const folder = user.sshConnections.find(folder => folder.folderName === folderName);
if (folder) {
folder.connections.push(hostConfig);
await user.save();
return { success: true };
socket.on('loginUser', async ({ username, password, sessionToken }, callback) => {
try {
let user;
if (sessionToken) {
user = await User.findOne({ sessionToken });
} else {
return { error: 'Folder not found' };
user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) {
logger.warn(`Invalid credentials for: ${username}`);
return callback({ error: 'Invalid credentials' });
}
}
} else {
return { error: 'User not found' };
}
} catch (err) {
return { error: 'Error moving host to folder: ' + err.message };
}
}
dbNamespace.on("connection", (socket) => {
console.log("New socket connection established on");
if (!user) {
logger.warn('Login failed - user not found');
return callback({ error: 'Invalid credentials' });
}
socket.on("createUser", async (data) => {
const { username, password } = data;
if (!username || !password) {
socket.emit("error", "Please provide both username and password");
return;
logger.info(`User logged in: ${user.username}`);
callback({ success: true, user: {
id: user._id,
username: user.username,
sessionToken: user.sessionToken
}});
} catch (error) {
logger.error('Login error:', error);
callback({ error: 'Login failed' });
}
const result = await createUser(username, password);
socket.emit(result.error ? "error" : "userCreated", result);
console.log(result.error || `User created`);
});
socket.on("loginUser", async (data) => {
const { username, password, sessionToken } = data;
let result;
if (sessionToken) {
result = await loginWithToken(sessionToken);
} else if (username && password) {
result = await loginUser(username, password);
} else {
socket.emit("error", "Please provide both username and password or a session token");
return;
socket.on('saveHostConfig', async ({ userId, sessionToken, hostConfig }, callback) => {
try {
if (!userId || !sessionToken) {
logger.warn('Missing authentication parameters');
return callback({ error: 'Authentication required' });
}
if (!hostConfig || typeof hostConfig !== 'object') {
logger.warn('Invalid host config format');
return callback({ error: 'Invalid host configuration' });
}
if (!hostConfig.ip || !hostConfig.user) {
logger.warn('Missing required fields:', hostConfig);
return callback({ error: 'IP and User are required' });
}
const user = await User.findOne({ _id: userId, sessionToken });
if (!user) {
logger.warn(`Invalid session for user: ${userId}`);
return callback({ error: 'Invalid session' });
}
const cleanConfig = {
name: hostConfig.name.trim(),
ip: hostConfig.ip.trim(),
user: hostConfig.user.trim(),
port: hostConfig.port || 22,
password: hostConfig.password?.trim() || undefined,
rsaKey: hostConfig.rsaKey?.trim() || undefined
};
const finalName = cleanConfig.name || cleanConfig.ip;
const existingHost = await Host.findOne({
name: finalName,
createdBy: userId
});
if (existingHost) {
logger.warn(`Host with name ${finalName} already exists for user: ${userId}`);
return callback({ error: 'Host with this name already exists' });
}
const encryptedConfig = encryptData(cleanConfig, userId, sessionToken);
if (!encryptedConfig) {
logger.error('Encryption failed for host config');
return callback({ error: 'Configuration encryption failed' });
}
await Host.create({
name: finalName,
config: encryptedConfig,
users: [userId],
createdBy: userId
});
logger.info(`Host created successfully: ${finalName}`);
callback({ success: true });
} catch (error) {
logger.error('Host save error:', error);
callback({ error: `Host save failed: ${error.message}` });
}
socket.emit(result.error ? "error" : "userFound", result);
console.log(result.error || `User logged in`);
});
socket.on("deleteUser", async (data) => {
const { userId } = data;
if (!userId) {
socket.emit("error", "User ID is required");
return;
socket.on('getHosts', async ({ userId, sessionToken }, callback) => {
try {
const user = await User.findOne({ _id: userId, sessionToken });
if (!user) {
logger.warn(`Invalid session for user: ${userId}`);
return callback({ error: 'Invalid session' });
}
const hosts = await Host.find({ users: userId });
const decryptedHosts = hosts.map(host => ({
...host.toObject(),
config: decryptData(host.config, userId, sessionToken)
})).filter(host => host.config);
callback({ success: true, hosts: decryptedHosts });
} catch (error) {
logger.error('Get hosts error:', error);
callback({ error: 'Failed to fetch hosts' });
}
const result = await deleteUser(userId);
socket.emit(result.error ? "error" : "userDeleted", result);
console.log(result.error || `User deleted`);
});
socket.on("saveHostConfig", async (data) => {
const { userId, hostConfig } = data;
if (!userId || !hostConfig) {
socket.emit("error", "User ID and host config are required");
return;
socket.on('deleteHost', async ({ userId, sessionToken, hostId }, callback) => {
try {
logger.debug(`Deleting host: ${hostId} for user: ${userId}`);
if (!userId || !sessionToken) {
logger.warn('Missing authentication parameters');
return callback({ error: 'Authentication required' });
}
if (!hostId || typeof hostId !== 'string') {
logger.warn('Invalid host ID format');
return callback({ error: 'Invalid host ID' });
}
const user = await User.findOne({ _id: userId, sessionToken });
if (!user) {
logger.warn(`Invalid session for user: ${userId}`);
return callback({ error: 'Invalid session' });
}
const result = await Host.deleteOne({ _id: hostId, createdBy: userId });
if (result.deletedCount === 0) {
logger.warn(`Host not found or not authorized: ${hostId}`);
return callback({ error: 'Host not found or not authorized' });
}
logger.info(`Host deleted: ${hostId}`);
callback({ success: true });
} catch (error) {
logger.error('Host deletion error:', error);
callback({ error: `Host deletion failed: ${error.message}` });
}
const result = await saveHostConfig(userId, hostConfig);
socket.emit(result.error ? "error" : "hostConfigSaved", result);
console.log(result.error || `Host config saved`);
});
socket.on("getHosts", async (data) => {
const { userId } = data;
if (!userId) {
socket.emit("error", "User ID is required");
return;
socket.on('shareHost', async ({ userId, sessionToken, hostId, targetUsername }, callback) => {
try {
logger.debug(`Sharing host ${hostId} with ${targetUsername}`);
const user = await User.findOne({ _id: userId, sessionToken });
if (!user) {
logger.warn(`Invalid session for user: ${userId}`);
return callback({ error: 'Invalid session' });
}
const targetUser = await User.findOne({ username: targetUsername });
if (!targetUser) {
logger.warn(`Target user not found: ${targetUsername}`);
return callback({ error: 'User not found' });
}
const host = await Host.findOne({ _id: hostId, createdBy: userId });
if (!host) {
logger.warn(`Host not found or unauthorized: ${hostId}`);
return callback({ error: 'Host not found' });
}
if (host.users.includes(targetUser._id)) {
logger.warn(`Host already shared with user: ${targetUsername}`);
return callback({ error: 'Already shared' });
}
host.users.push(targetUser._id);
await host.save();
logger.info(`Host shared successfully: ${hostId} -> ${targetUsername}`);
callback({ success: true });
} catch (error) {
logger.error('Host sharing error:', error);
callback({ error: 'Failed to share host' });
}
const result = await getHosts(userId);
socket.emit(result.error ? "error" : "hostsFound", result);
console.log(result.error || `Hosts found`);
});
socket.on("deleteHost", async (data) => {
const { userId, hostConfig } = data;
if (!userId || !hostConfig) {
socket.emit("error", "User ID and host config are required");
return;
socket.on('deleteUser', async ({ userId, sessionToken }, callback) => {
try {
logger.debug(`Deleting user: ${userId}`);
const user = await User.findOne({ _id: userId, sessionToken });
if (!user) {
logger.warn(`Invalid session for user: ${userId}`);
return callback({ error: 'Invalid session' });
}
await Host.deleteMany({ createdBy: userId });
await User.deleteOne({ _id: userId });
logger.info(`User deleted: ${userId}`);
callback({ success: true });
} catch (error) {
logger.error('User deletion error:', error);
callback({ error: 'Failed to delete user' });
}
const result = await deleteHost(userId, hostConfig);
socket.emit(result.error ? "error" : "hostDeleted", result);
console.log(result.error || `Host deleted`);
});
socket.on("editHost", async (data) => {
const { userId, oldHostConfig, newHostConfig } = data;
if (!userId || !oldHostConfig || !newHostConfig) {
socket.emit("error", "User ID, old host config, and new host config are required");
return;
socket.on("editHost", async ({ userId, sessionToken, oldHostConfig, newHostConfig }, callback) => {
try {
logger.debug(`Editing host for user: ${userId}`);
if (!oldHostConfig || !newHostConfig) {
logger.warn('Missing host configurations');
return callback({ error: 'Missing host configurations' });
}
const user = await User.findOne({ _id: userId, sessionToken });
if (!user) {
logger.warn(`Invalid session for user: ${userId}`);
return callback({ error: 'Invalid session' });
}
const hosts = await Host.find({ createdBy: userId });
const host = hosts.find(h => {
const decryptedConfig = decryptData(h.config, userId, sessionToken);
return decryptedConfig && decryptedConfig.ip === oldHostConfig.ip;
});
if (!host) {
logger.warn(`Host not found or unauthorized`);
return callback({ error: 'Host not found' });
}
const cleanConfig = {
ip: newHostConfig.ip.trim(),
user: newHostConfig.user.trim(),
port: newHostConfig.port || 22,
name: newHostConfig.name.trim(),
password: newHostConfig.password?.trim() || undefined,
rsaKey: newHostConfig.rsaKey?.trim() || undefined
};
const encryptedConfig = encryptData(cleanConfig, userId, sessionToken);
if (!encryptedConfig) {
logger.error('Encryption failed for host config');
return callback({ error: 'Configuration encryption failed' });
}
host.config = encryptedConfig;
await host.save();
logger.info(`Host edited successfully`);
callback({ success: true });
} catch (error) {
logger.error('Host edit error:', error);
callback({ error: 'Failed to edit host' });
}
const result = await editHost(userId, oldHostConfig, newHostConfig);
socket.emit(result.error ? "error" : "hostEdited", result);
console.log(result.error || `Host edited`);
});
socket.on("createFolder", async (data) => {
const { userId, folderName } = data;
if (!userId || !folderName) {
socket.emit("error", "User ID and folder name are required");
return;
}
const result = await createFolder(userId, folderName);
socket.emit(result.error ? "error" : "folderCreated", result);
console.log(result.error || `Folder created`);
});
socket.on('verifySession', async ({ sessionToken }, callback) => {
try {
const user = await User.findOne({ sessionToken });
if (!user) {
logger.warn(`Invalid session token: ${sessionToken}`);
return callback({ error: 'Invalid session' });
}
socket.on("moveHostToFolder", async (data) => {
const { userId, hostConfig, folderName } = data;
if (!userId || !hostConfig || !folderName) {
socket.emit("error", "User ID, host config, and folder name are required");
return;
callback({ success: true, user: {
id: user._id,
username: user.username
}});
} catch (error) {
logger.error('Session verification error:', error);
callback({ error: 'Session verification failed' });
}
const result = await moveHostToFolder(userId, hostConfig, folderName);
socket.emit(result.error ? "error" : "hostMoved", result);
console.log(result.error || `Host moved to folder`);
});
});
server.listen(8082, '0.0.0.0', async () => {
console.log("Server is running on port 8082");
await connectToMongoDB();
server.listen(8082, () => {
logger.info('Server running on port 8082');
});

View File

@@ -4,27 +4,33 @@ const SSHClient = require("ssh2").Client;
const server = http.createServer();
const io = socketIo(server, {
path: "/ssh.io/socket.io", // Corrected path for socket.io
path: "/ssh.io/socket.io",
cors: {
origin: "*", // Temporarily set to '*' to allow all origins. Change to specific URLs if needed.
origin: "*",
methods: ["GET", "POST"],
credentials: true
},
allowEIO3: true
});
const logger = {
info: (...args) => console.log(`🔧 [${new Date().toISOString()}] INFO:`, ...args),
error: (...args) => console.error(`❌ [${new Date().toISOString()}] ERROR:`, ...args),
warn: (...args) => console.warn(`⚠️ [${new Date().toISOString()}] WARN:`, ...args),
debug: (...args) => console.debug(`🔍 [${new Date().toISOString()}] DEBUG:`, ...args)
};
io.on("connection", (socket) => {
console.log("New socket connection established");
logger.info("New socket connection established");
let stream = null;
socket.on("connectToHost", (cols, rows, hostConfig) => {
if (!hostConfig || !hostConfig.ip || !hostConfig.user || (!hostConfig.password && !hostConfig.rsaKey) || !hostConfig.port) {
console.error("Invalid hostConfig received:", hostConfig);
logger.error("Invalid hostConfig received:", hostConfig);
return;
}
// Redact only sensitive info for logging
const safeHostConfig = {
ip: hostConfig.ip,
port: hostConfig.port,
@@ -33,57 +39,52 @@ io.on("connection", (socket) => {
rsaKey: hostConfig.rsaKey ? '***REDACTED***' : undefined,
};
console.log("Received hostConfig:", safeHostConfig);
logger.info("Received hostConfig:", safeHostConfig);
const { ip, port, user, password, rsaKey } = hostConfig;
const conn = new SSHClient();
conn
.on("ready", function () {
console.log("SSH connection established");
logger.info("SSH connection established");
conn.shell({ term: "xterm-256color" }, function (err, newStream) {
if (err) {
console.error("Error:", err.message);
logger.error("Error:", err.message);
socket.emit("error", err.message);
return;
}
stream = newStream;
// Set initial terminal size
stream.setWindow(rows, cols, rows * 100, cols * 100);
// Pipe SSH output to client
stream.on("data", function (data) {
socket.emit("data", data);
});
stream.on("close", function () {
console.log("SSH stream closed");
logger.info("SSH stream closed");
conn.end();
});
// Send keystrokes from terminal to SSH
socket.on("data", function (data) {
stream.write(data);
});
// Resize SSH terminal when client resizes
socket.on("resize", ({ cols, rows }) => {
if (stream && stream.setWindow) {
stream.setWindow(rows, cols, rows * 100, cols * 100);
}
});
// Auto-send initial terminal size to backend
socket.emit("resize", { cols, rows });
});
})
.on("close", function () {
console.log("SSH connection closed");
logger.info("SSH connection closed");
socket.emit("error", "SSH connection closed");
})
.on("error", function (err) {
console.error("Error:", err.message);
logger.error("Error:", err.message);
socket.emit("error", err.message);
})
.connect({
@@ -96,10 +97,10 @@ io.on("connection", (socket) => {
});
socket.on("disconnect", () => {
console.log("Client disconnected");
logger.info("Client disconnected");
});
});
server.listen(8081, '0.0.0.0', () => {
console.log("Server is running on port 8081");
logger.info("Server is running on port 8081");
});

View File

@@ -20,7 +20,7 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
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')) {
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') || file.name.endsWith('.pub')) {
const reader = new FileReader();
reader.onload = (event) => {
setForm({ ...form, rsaKey: event.target.result });
@@ -65,7 +65,9 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
<form
onSubmit={(event) => {
event.preventDefault();
if (isFormValid()) handleAddHost();
if (isFormValid()) {
handleAddHost();
}
}}
>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>
@@ -176,10 +178,13 @@ const AddHostModal = ({ isHidden, form, setForm, handleAddHost, setIsAddHostHidd
<FormControl>
<FormLabel>Remember Host</FormLabel>
<Checkbox
checked={form.rememberHost ?? false}
checked={form.rememberHost}
onChange={(e) => setForm({ ...form, rememberHost: e.target.checked })}
sx={{
color: theme.palette.text.primary,
'&.Mui-checked': {
color: theme.palette.text.primary,
},
}}
/>
</FormControl>

View File

@@ -12,8 +12,7 @@ import {
DialogContent,
ModalDialog,
Select,
Option,
Checkbox
Option
} from '@mui/joy';
import theme from '/src/theme';
@@ -41,6 +40,11 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
return true;
};
const handleEditHostInternal = (form) => {
const updatedForm = { ...form, name: form.name || form.ip };
handleEditHost(updatedForm);
};
useEffect(() => {
if (hostConfig) {
setForm({
@@ -81,7 +85,7 @@ const EditHostModal = ({ isHidden, form, setForm, handleEditHost, setIsEditHostH
<form
onSubmit={(event) => {
event.preventDefault();
if (isFormValid()) handleEditHost();
if (isFormValid()) handleEditHostInternal(form);
}}
>
<Stack spacing={2} sx={{ width: "100%", maxWidth: "100%", overflow: "hidden" }}>