* Added user system with database features. This is fairly experimental and does not include dockerfile to automatically generate a mongodb. This should be in future commits along with ability to save hosts branching off this database feature.

* Updated README, fixed a few bugs with user creation, and added docker support to run MongoDB (needs testing)

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in installing MongoDB

* Changes to Dockerfile to fix error in connecting to sockets

* Update README.md

* Changes to connection system to support docker

* Changes to connection system to support docker

* Changes to connection system to support docker

* Changes to connection system to support docker

* Save hosts to tabs (very early version, not that many issues not just not very feature rich and has a poor UI that will be improved with more features)

* Updated launchpad UI to be expandable in the future. Updated UI for the hosts to be able to easily configure them. They stil need organizational system (folders, etc.)

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

* Updated database connection method.

* Updated Profile modal to show username text more clearly

* Updated Profile modal to show username text more clearly

* Fixed control v pasting formating. Reorganized location of scripts. Visbile password and confirm password. Guest login. OpenSSH key authentication. Optional to remember password. Serach for host viewer.

* Waits for user to be able to log in. Improved UI for profile, edit and add host, and added organizational features to the host app (Folders, search, etc.)

* Updated various names for rsa keys to public keys, fixes ssh not connecting, better timing for editing host.

* Added ability to share hosts. Fixed up overall UI errors in console and cleaned up code for release.

* Fix GitHub build errors

* Attempt #1 to auto compile MongoDB into the build to exclude it from the compose.

* Attempt #2 to auto compile MongoDB into the build to exclude it from the compose.

* Attempt #3 to auto compile MongoDB into the build to exclude it from the compose.

* Attempt #3 to auto compile MongoDB into the build to exclude it from the compose.
This commit was merged in pull request #23.
This commit is contained in:
Karmaa
2025-03-16 14:17:55 -05:00
committed by GitHub
parent 9aa83c24ed
commit 10bc491a9f
33 changed files with 4820 additions and 464 deletions

460
src/backend/database.cjs Normal file
View File

@@ -0,0 +1,460 @@
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'] }
});
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
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' },
folder: { type: String, default: null }
});
const User = mongoose.model('User', userSchema);
const Host = mongoose.model('Host', hostSchema);
const getEncryptionKey = (userId, sessionToken) => {
const salt = process.env.SALT || 'default_salt';
return crypto.scryptSync(`${userId}-${sessionToken}`, salt, 32);
};
const encryptData = (data, userId, sessionToken) => {
try {
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;
}
};
const decryptData = (encryptedData, userId, sessionToken) => {
try {
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' });
}
const sessionToken = crypto.randomBytes(64).toString('hex');
const user = await User.create({
username,
password: await bcrypt.hash(password, 10),
sessionToken
});
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' });
}
});
socket.on('loginUser', async ({ username, password, sessionToken }, callback) => {
try {
let user;
if (sessionToken) {
user = await User.findOne({ sessionToken });
} else {
user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) {
logger.warn(`Invalid credentials for: ${username}`);
return callback({ error: 'Invalid credentials' });
}
}
if (!user) {
logger.warn('Login failed - user not found');
return callback({ error: 'Invalid credentials' });
}
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' });
}
});
socket.on('loginAsGuest', async (callback) => {
try {
const username = `guest-${crypto.randomBytes(4).toString('hex')}`;
const sessionToken = crypto.randomBytes(64).toString('hex');
const user = await User.create({
username,
password: await bcrypt.hash(username, 10),
sessionToken
});
logger.info(`Guest user created: ${username}`);
callback({ success: true, user: {
id: user._id,
username: user.username,
sessionToken
}});
} catch (error) {
logger.error('Guest login error:', error);
callback({error: 'Guest login failed'});
}
});
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(),
folder: hostConfig.folder?.trim() || null,
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,
folder: cleanConfig.folder
});
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.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 }).populate('createdBy');
const decryptedHosts = await Promise.all(hosts.map(async host => {
try {
const ownerUser = host.createdBy;
if (!ownerUser) {
logger.warn(`Owner not found for host: ${host._id}`);
return null;
}
const decryptedConfig = decryptData(host.config, ownerUser._id.toString(), ownerUser.sessionToken);
if (!decryptedConfig) {
logger.warn(`Failed to decrypt host config for host: ${host._id}`);
return null;
}
return {
...host.toObject(),
config: decryptedConfig
};
} catch (error) {
logger.error(`Failed to process host ${host._id}:`, error);
return null;
}
}));
callback({ success: true, hosts: decryptedHosts.filter(host => host && host.config) });
} catch (error) {
logger.error('Get hosts error:', error);
callback({ error: 'Failed to fetch hosts' });
}
});
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}` });
}
});
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' });
}
});
socket.on('removeShare', async ({ userId, sessionToken, hostId }, callback) => {
try {
logger.debug(`Removing share for host ${hostId} from user ${userId}`);
const user = await User.findOne({ _id: userId, sessionToken });
if (!user) {
logger.warn(`Invalid session for user: ${userId}`);
return callback({ error: 'Invalid session' });
}
const host = await Host.findById(hostId);
if (!host) {
logger.warn(`Host not found: ${hostId}`);
return callback({ error: 'Host not found' });
}
host.users = host.users.filter(id => id.toString() !== userId);
await host.save();
logger.info(`Share removed successfully: ${hostId} -> ${userId}`);
callback({ success: true });
} catch (error) {
logger.error('Share removal error:', error);
callback({ error: 'Failed to remove share' });
}
});
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' });
}
});
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 = {
name: newHostConfig.name?.trim(),
folder: newHostConfig.folder?.trim() || null,
ip: newHostConfig.ip.trim(),
user: newHostConfig.user.trim(),
port: newHostConfig.port || 22,
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;
host.folder = cleanConfig.folder;
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' });
}
});
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' });
}
callback({ success: true, user: {
id: user._id,
username: user.username
}});
} catch (error) {
logger.error('Session verification error:', error);
callback({ error: 'Session verification failed' });
}
});
});
server.listen(8082, () => {
logger.info('Server running on port 8082');
});

View File

@@ -4,6 +4,7 @@ const SSHClient = require("ssh2").Client;
const server = http.createServer();
const io = socketIo(server, {
path: "/ssh.io/socket.io",
cors: {
origin: "*",
methods: ["GET", "POST"],
@@ -12,77 +13,84 @@ const io = socketIo(server, {
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);
if (!hostConfig || !hostConfig.ip || !hostConfig.user || !hostConfig.port) {
logger.error("Invalid hostConfig received - missing required fields:", hostConfig);
socket.emit("error", "Missing required connection details (IP, user, or port)");
return;
}
if (!hostConfig.password && !hostConfig.rsaKey) {
logger.error("No authentication provided");
socket.emit("error", "Authentication required");
return;
}
// Redact only sensitive info for logging
const safeHostConfig = {
ip: hostConfig.ip,
port: hostConfig.port,
user: hostConfig.user,
password: hostConfig.password ? '***REDACTED***' : undefined,
rsaKey: hostConfig.rsaKey ? '***REDACTED***' : undefined,
authType: hostConfig.password ? 'password' : 'public key',
};
console.log("Received hostConfig:", safeHostConfig);
logger.info("Connecting with config:", 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("Shell 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({
@@ -95,10 +103,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");
});