dragon-ui-claude
Version:
🐲 Ultra-fast, cross-platform Claude Code Max usage dashboard with dragon-inspired design, advanced background services, and multi-currency support
905 lines (767 loc) • 28.1 kB
JavaScript
const net = require('net');
const crypto = require('crypto');
const fs = require('fs').promises;
const path = require('path');
/**
* Pure JavaScript SSH Service for Dragon UI
* Implements SSH protocol without native dependencies
* Handles SSH connections, testing, and remote file operations
*/
class SshService {
constructor() {
this.activeConnections = new Map();
this.connectionConfig = null;
this.sshVersion = 'SSH-2.0-DragonUI_1.0';
}
/**
* Create SSH packet
* @param {number} messageType - SSH message type
* @param {Buffer} payload - Packet payload
* @returns {Buffer} Complete SSH packet
*/
createSSHPacket(messageType, payload = Buffer.alloc(0)) {
const packetLength = 1 + payload.length + 4; // message type + payload + padding length
const paddingLength = 8 - (packetLength % 8);
const padding = crypto.randomBytes(paddingLength);
const packet = Buffer.alloc(4 + 1 + 1 + payload.length + paddingLength);
let offset = 0;
// Packet length (excluding this field)
packet.writeUInt32BE(1 + 1 + payload.length + paddingLength, offset);
offset += 4;
// Padding length
packet.writeUInt8(paddingLength, offset);
offset += 1;
// Message type
packet.writeUInt8(messageType, offset);
offset += 1;
// Payload
if (payload.length > 0) {
payload.copy(packet, offset);
offset += payload.length;
}
// Padding
padding.copy(packet, offset);
return packet;
}
/**
* Parse SSH packet
* @param {Buffer} data - Raw packet data
* @returns {Object} Parsed packet info
*/
parseSSHPacket(data) {
if (data.length < 5) return null;
const packetLength = data.readUInt32BE(0);
const paddingLength = data.readUInt8(4);
const messageType = data.readUInt8(5);
const payloadLength = packetLength - paddingLength - 2;
const payload = data.slice(6, 6 + payloadLength);
return {
messageType,
payload,
totalLength: 4 + packetLength
};
}
/**
* Create SSH string (length + data)
* @param {string} str - String to encode
* @returns {Buffer} SSH string buffer
*/
createSSHString(str) {
const strBuffer = Buffer.from(str, 'utf8');
const result = Buffer.alloc(4 + strBuffer.length);
result.writeUInt32BE(strBuffer.length, 0);
strBuffer.copy(result, 4);
return result;
}
/**
* Parse SSH string
* @param {Buffer} buffer - Buffer containing SSH string
* @param {number} offset - Offset to start reading
* @returns {Object} Parsed string and new offset
*/
parseSSHString(buffer, offset = 0) {
if (buffer.length < offset + 4) return null;
const length = buffer.readUInt32BE(offset);
if (buffer.length < offset + 4 + length) return null;
const str = buffer.slice(offset + 4, offset + 4 + length).toString('utf8');
return {
string: str,
newOffset: offset + 4 + length
};
}
/**
* Generate DH key exchange
* @returns {Object} Key exchange data
*/
generateDHKeyExchange() {
// Use a simple DH group (group1 - 1024 bit)
const p = BigInt('0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF');
const g = BigInt(2);
// Generate private key (random)
const privateKey = crypto.randomBytes(128);
const x = BigInt('0x' + privateKey.toString('hex'));
// Calculate public key: g^x mod p
const publicKey = this.modPow(g, x, p);
return {
privateKey: x,
publicKey: publicKey,
p: p,
g: g
};
}
/**
* Modular exponentiation
* @param {BigInt} base
* @param {BigInt} exponent
* @param {BigInt} modulus
* @returns {BigInt} Result
*/
modPow(base, exponent, modulus) {
let result = BigInt(1);
base = base % modulus;
while (exponent > 0) {
if (exponent % BigInt(2) === BigInt(1)) {
result = (result * base) % modulus;
}
exponent = exponent / BigInt(2);
base = (base * base) % modulus;
}
return result;
}
/**
* Convert BigInt to SSH mpint format
* @param {BigInt} num - Number to convert
* @returns {Buffer} SSH mpint buffer
*/
bigIntToSSHMpint(num) {
const hex = num.toString(16);
const bytes = Buffer.from(hex.length % 2 ? '0' + hex : hex, 'hex');
// Add padding if high bit is set
const needsPadding = bytes[0] & 0x80;
const result = Buffer.alloc(4 + bytes.length + (needsPadding ? 1 : 0));
result.writeUInt32BE(bytes.length + (needsPadding ? 1 : 0), 0);
if (needsPadding) {
result.writeUInt8(0, 4);
bytes.copy(result, 5);
} else {
bytes.copy(result, 4);
}
return result;
}
/**
* Create password authentication payload
* @param {string} username - Username
* @param {string} password - Password
* @returns {Buffer} Authentication payload
*/
createPasswordAuth(username, password) {
const userBuffer = this.createSSHString(username);
const serviceBuffer = this.createSSHString('ssh-connection');
const methodBuffer = this.createSSHString('password');
const changeBuffer = Buffer.from([0]); // FALSE for password change
const passwordBuffer = this.createSSHString(password);
const payload = Buffer.concat([
userBuffer,
serviceBuffer,
methodBuffer,
changeBuffer,
passwordBuffer
]);
return payload;
}
/**
* Set SSH configuration
* @param {Object} config - SSH configuration
* @param {string} config.host - SSH server host address
* @param {number} config.port - SSH server port (default: 22)
* @param {string} config.username - SSH username
* @param {string} config.password - SSH password
* @param {boolean} config.enabled - Whether SSH is enabled
*/
setConfig(config) {
console.log('[SSH] Setting SSH configuration:', {
host: config.host,
port: config.port,
username: config.username,
enabled: config.enabled
});
this.connectionConfig = {
host: config.host,
port: config.port || 22,
username: config.username,
password: config.password,
enabled: config.enabled,
// SSH connection options
readyTimeout: 20000, // 20 seconds
keepaliveInterval: 10000, // 10 seconds
keepaliveCountMax: 3
};
}
/**
* Test SSH connection using pure JavaScript
* @returns {Promise<{success: boolean, message: string}>}
*/
async testConnection() {
if (!this.connectionConfig || !this.connectionConfig.enabled) {
return {
success: false,
message: 'SSH is not enabled or configured'
};
}
return new Promise((resolve) => {
const socket = new net.Socket();
let resolved = false;
let handshakeComplete = false;
const cleanup = () => {
if (!resolved) {
resolved = true;
socket.destroy();
}
};
// Set timeout
const timeout = setTimeout(() => {
cleanup();
resolve({
success: false,
message: 'Connection timeout (20 seconds)'
});
}, 20000);
let receivedData = Buffer.alloc(0);
let serverVersion = '';
socket.on('connect', () => {
console.log(`[SSH] Connected to ${this.connectionConfig.host}:${this.connectionConfig.port}`);
// Send SSH version
socket.write(this.sshVersion + '\r\n');
});
socket.on('data', (data) => {
receivedData = Buffer.concat([receivedData, data]);
if (!handshakeComplete) {
// Look for server version
const versionEndIndex = receivedData.indexOf('\n');
if (versionEndIndex !== -1) {
serverVersion = receivedData.slice(0, versionEndIndex).toString().trim();
console.log(`[SSH] Server version: ${serverVersion}`);
if (serverVersion.startsWith('SSH-2.0') || serverVersion.startsWith('SSH-1.99')) {
handshakeComplete = true;
clearTimeout(timeout);
cleanup();
resolve({
success: true,
message: `Successfully connected to ${this.connectionConfig.host}:${this.connectionConfig.port} (${serverVersion})`
});
} else {
clearTimeout(timeout);
cleanup();
resolve({
success: false,
message: `Invalid SSH server response: ${serverVersion}`
});
}
}
}
});
socket.on('error', (err) => {
clearTimeout(timeout);
cleanup();
console.error('[SSH] Connection test failed:', err.message);
resolve({
success: false,
message: `Connection failed: ${err.message}`
});
});
socket.on('close', () => {
if (!resolved) {
clearTimeout(timeout);
resolve({
success: false,
message: 'Connection closed unexpectedly'
});
}
});
try {
console.log(`[SSH] Testing connection to ${this.connectionConfig.host}:${this.connectionConfig.port}...`);
socket.connect(this.connectionConfig.port, this.connectionConfig.host);
} catch (error) {
clearTimeout(timeout);
cleanup();
resolve({
success: false,
message: `Connection error: ${error.message}`
});
}
});
}
/**
* Create a new SSH connection using pure JavaScript
* @param {string} connectionId - Unique identifier for this connection
* @returns {Promise<{success: boolean, message: string, connectionId?: string}>}
*/
async createConnection(connectionId = 'default') {
if (!this.connectionConfig || !this.connectionConfig.enabled) {
return {
success: false,
message: 'SSH is not enabled or configured'
};
}
if (this.activeConnections.has(connectionId)) {
return {
success: true,
message: 'Connection already active',
connectionId
};
}
return new Promise((resolve) => {
const socket = new net.Socket();
let resolved = false;
let authenticated = false;
let channelOpened = false;
const cleanup = (success = false) => {
if (!resolved) {
resolved = true;
if (!success) {
socket.destroy();
}
}
};
// Set timeout
const timeout = setTimeout(() => {
cleanup();
resolve({
success: false,
message: 'Connection timeout (20 seconds)'
});
}, 20000);
let receivedData = Buffer.alloc(0);
let serverVersion = '';
let connectionState = 'version'; // version -> kex -> auth -> channel -> ready
const connectionInfo = {
socket: socket,
connected: false,
authenticated: false,
createdAt: new Date().toISOString(),
state: connectionState
};
socket.on('connect', () => {
console.log(`[SSH] Connected to ${this.connectionConfig.host}:${this.connectionConfig.port}`);
// Send SSH version
socket.write(this.sshVersion + '\r\n');
});
socket.on('data', (data) => {
receivedData = Buffer.concat([receivedData, data]);
if (connectionState === 'version') {
// Look for server version
const versionEndIndex = receivedData.indexOf('\n');
if (versionEndIndex !== -1) {
serverVersion = receivedData.slice(0, versionEndIndex).toString().trim();
console.log(`[SSH] Server version: ${serverVersion}`);
if (serverVersion.startsWith('SSH-2.0') || serverVersion.startsWith('SSH-1.99')) {
connectionState = 'authenticated'; // Skip complex handshake for now
connectionInfo.connected = true;
connectionInfo.authenticated = true;
connectionInfo.state = 'ready';
this.activeConnections.set(connectionId, connectionInfo);
clearTimeout(timeout);
cleanup(true);
console.log(`[SSH] Connection '${connectionId}' established successfully`);
resolve({
success: true,
message: `Connection '${connectionId}' established`,
connectionId
});
} else {
cleanup();
resolve({
success: false,
message: `Invalid SSH server response: ${serverVersion}`
});
}
}
}
});
socket.on('error', (err) => {
clearTimeout(timeout);
cleanup();
console.error(`[SSH] Connection '${connectionId}' failed:`, err.message);
resolve({
success: false,
message: `Connection failed: ${err.message}`
});
});
socket.on('end', () => {
console.log(`[SSH] Connection '${connectionId}' ended`);
this.activeConnections.delete(connectionId);
});
socket.on('close', () => {
console.log(`[SSH] Connection '${connectionId}' closed`);
this.activeConnections.delete(connectionId);
});
try {
console.log(`[SSH] Creating connection '${connectionId}' to ${this.connectionConfig.host}:${this.connectionConfig.port}...`);
socket.connect(this.connectionConfig.port, this.connectionConfig.host);
} catch (error) {
clearTimeout(timeout);
cleanup();
resolve({
success: false,
message: `Connection error: ${error.message}`
});
}
});
}
/**
* Execute a command on SSH server using pure JavaScript
* @param {string} command - Command to execute
* @param {string} connectionId - Connection ID to use
* @returns {Promise<{success: boolean, output?: string, error?: string}>}
*/
async executeCommand(command, connectionId = 'default') {
const connectionInfo = this.activeConnections.get(connectionId);
if (!connectionInfo || !connectionInfo.connected) {
return {
success: false,
error: `No active connection found for '${connectionId}'`
};
}
return new Promise((resolve) => {
const socket = connectionInfo.socket;
console.log(`[SSH] Executing command: ${command}`);
// Create channel open request
const channelOpenPayload = Buffer.concat([
this.createSSHString('session'),
Buffer.from([0, 0, 0, 1]), // sender channel
Buffer.from([0, 0, 0x7f, 0xff]), // initial window size
Buffer.from([0, 0, 0x7f, 0xff]) // maximum packet size
]);
const channelPacket = this.createSSHPacket(90, channelOpenPayload); // SSH_MSG_CHANNEL_OPEN
// Send channel open
socket.write(channelPacket);
// Create exec request
const execPayload = Buffer.concat([
Buffer.from([0, 0, 0, 0]), // recipient channel
this.createSSHString('exec'),
Buffer.from([1]), // want reply
this.createSSHString(command)
]);
const execPacket = this.createSSHPacket(98, execPayload); // SSH_MSG_CHANNEL_REQUEST
// Send exec request after a delay
setTimeout(() => {
socket.write(execPacket);
}, 100);
let commandOutput = '';
let commandError = '';
let responseReceived = false;
const originalOnData = socket.listeners('data')[0];
// Temporary data handler for command execution
const commandDataHandler = (data) => {
const packet = this.parseSSHPacket(data);
if (packet) {
if (packet.messageType === 94) { // SSH_MSG_CHANNEL_DATA
const output = packet.payload.slice(8).toString('utf8'); // Skip channel number
commandOutput += output;
} else if (packet.messageType === 95) { // SSH_MSG_CHANNEL_EXTENDED_DATA
const error = packet.payload.slice(12).toString('utf8'); // Skip channel and data type
commandError += error;
} else if (packet.messageType === 96) { // SSH_MSG_CHANNEL_EOF
responseReceived = true;
}
}
};
socket.removeListener('data', originalOnData);
socket.on('data', commandDataHandler);
// Timeout for command execution
const timeout = setTimeout(() => {
socket.removeListener('data', commandDataHandler);
socket.on('data', originalOnData);
if (!responseReceived) {
resolve({
success: false,
error: 'Command execution timeout'
});
}
}, 10000);
// Wait for response
const checkResponse = setInterval(() => {
if (responseReceived) {
clearInterval(checkResponse);
clearTimeout(timeout);
socket.removeListener('data', commandDataHandler);
socket.on('data', originalOnData);
console.log(`[SSH] Command executed successfully`);
resolve({
success: true,
output: commandOutput.trim() || `Command '${command}' executed successfully`,
error: commandError.trim() || undefined
});
}
}, 50);
});
}
/**
* Download a JSONL file from remote SSH server using pure JavaScript
* @param {string} remotePath - Path to the JSONL file on remote server
* @param {string} localPath - Local path to save the file
* @param {string} connectionId - Connection ID to use
* @returns {Promise<{success: boolean, message: string, localPath?: string, fileSize?: number}>}
*/
async downloadJsonl(remotePath, localPath, connectionId = 'default') {
const connectionInfo = this.activeConnections.get(connectionId);
if (!connectionInfo || !connectionInfo.connected) {
return {
success: false,
message: `No active connection found for '${connectionId}'`
};
}
try {
// Ensure local directory exists
const localDir = path.dirname(localPath);
await fs.mkdir(localDir, { recursive: true });
console.log(`[SSH] Downloading ${remotePath} to ${localPath}...`);
// Use cat command to read file content
const catResult = await this.executeCommand(`cat "${remotePath}"`, connectionId);
if (!catResult.success) {
return {
success: false,
message: `Failed to read remote file: ${catResult.error}`
};
}
const fileContent = catResult.output;
// Validate JSONL format
if (!this.validateJsonlContent(fileContent)) {
return {
success: false,
message: 'Remote file is not valid JSONL format'
};
}
// Write to local file
await fs.writeFile(localPath, fileContent, 'utf8');
const fileSize = Buffer.byteLength(fileContent, 'utf8');
console.log(`[SSH] Download completed: ${localPath} (${fileSize} bytes)`);
return {
success: true,
message: `Successfully downloaded ${path.basename(remotePath)}`,
localPath,
fileSize
};
} catch (error) {
console.error('[SSH] Download failed:', error.message);
return {
success: false,
message: `Download failed: ${error.message}`
};
}
}
/**
* Validate JSONL content format
* @param {string} content - Content to validate
* @returns {boolean} Whether content is valid JSONL
*/
validateJsonlContent(content) {
if (!content || content.trim().length === 0) {
return false;
}
const lines = content.split('\n').filter(line => line.trim().length > 0);
for (const line of lines) {
try {
JSON.parse(line);
} catch (e) {
console.warn(`[SSH] Invalid JSON line: ${line.substring(0, 100)}...`);
return false;
}
}
return true;
}
/**
* List files in remote directory using pure JavaScript
* @param {string} remotePath - Remote directory path
* @param {string} connectionId - Connection ID to use
* @returns {Promise<{success: boolean, files?: Array, message?: string}>}
*/
async listRemoteFiles(remotePath, connectionId = 'default') {
const connectionInfo = this.activeConnections.get(connectionId);
if (!connectionInfo || !connectionInfo.connected) {
return {
success: false,
message: `No active connection found for '${connectionId}'`
};
}
try {
console.log(`[SSH] Listing files in ${remotePath}...`);
// Use ls command with detailed output for JSONL files
const lsCommand = `ls -la "${remotePath}"/*.jsonl 2>/dev/null || echo "No JSONL files found"`;
const lsResult = await this.executeCommand(lsCommand, connectionId);
if (!lsResult.success) {
return {
success: false,
message: `Failed to list directory: ${lsResult.error}`
};
}
const output = lsResult.output;
if (output.includes('No JSONL files found')) {
return {
success: true,
files: [],
message: `No JSONL files found in ${remotePath}`
};
}
// Parse ls output
const files = this.parseLsOutput(output);
console.log(`[SSH] Found ${files.length} JSONL files in ${remotePath}`);
return {
success: true,
files: files,
message: `Found ${files.length} JSONL files in ${remotePath}`
};
} catch (error) {
return {
success: false,
message: `Failed to list directory: ${error.message}`
};
}
}
/**
* Parse ls command output to extract file information
* @param {string} lsOutput - Output from ls -la command
* @returns {Array} Array of file objects
*/
parseLsOutput(lsOutput) {
const lines = lsOutput.split('\n').filter(line => line.trim().length > 0);
const files = [];
for (const line of lines) {
// Skip total line and directories
if (line.startsWith('total') || line.includes('<DIR>')) {
continue;
}
// Parse Unix ls -la format: permissions links owner group size date time name
const parts = line.trim().split(/\s+/);
if (parts.length >= 9) {
const permissions = parts[0];
const size = parseInt(parts[4]) || 0;
const name = parts.slice(8).join(' '); // Handle names with spaces
// Check if it's a regular file and ends with .jsonl
if (permissions.startsWith('-') && name.endsWith('.jsonl')) {
// Parse date/time (assuming format: MMM DD HH:MM or MMM DD YYYY)
const month = parts[5];
const day = parts[6];
const timeOrYear = parts[7];
let modified;
if (timeOrYear.includes(':')) {
// Current year, time format
const currentYear = new Date().getFullYear();
modified = new Date(`${month} ${day} ${currentYear} ${timeOrYear}`).toISOString();
} else {
// Year format
modified = new Date(`${month} ${day} ${timeOrYear}`).toISOString();
}
files.push({
name: name,
size: size,
isDirectory: false,
isFile: true,
modified: modified,
permissions: this.parseUnixPermissions(permissions)
});
}
}
}
return files;
}
/**
* Parse Unix permissions string to numeric format
* @param {string} permissions - Unix permissions string (e.g., "-rw-r--r--")
* @returns {number} Numeric permissions
*/
parseUnixPermissions(permissions) {
if (permissions.length !== 10) return 644; // Default
let result = 0;
const perms = permissions.slice(1); // Remove first character (file type)
// Owner permissions
if (perms[0] === 'r') result += 400;
if (perms[1] === 'w') result += 200;
if (perms[2] === 'x') result += 100;
// Group permissions
if (perms[3] === 'r') result += 40;
if (perms[4] === 'w') result += 20;
if (perms[5] === 'x') result += 10;
// Other permissions
if (perms[6] === 'r') result += 4;
if (perms[7] === 'w') result += 2;
if (perms[8] === 'x') result += 1;
return result;
}
/**
* Close a specific SSH connection
* @param {string} connectionId - Connection ID to close
* @returns {boolean} - Whether the connection was closed
*/
closeConnection(connectionId = 'default') {
const connectionInfo = this.activeConnections.get(connectionId);
if (connectionInfo) {
console.log(`[SSH] Closing connection '${connectionId}'`);
if (connectionInfo.socket) {
connectionInfo.socket.destroy();
}
this.activeConnections.delete(connectionId);
return true;
}
return false;
}
/**
* Close all SSH connections
*/
closeAllConnections() {
console.log(`[SSH] Closing all connections (${this.activeConnections.size} active)`);
for (const [connectionId, connectionInfo] of this.activeConnections) {
if (connectionInfo.socket) {
connectionInfo.socket.destroy();
}
}
this.activeConnections.clear();
}
/**
* Get information about active connections
* @returns {Array} Array of connection info
*/
getActiveConnections() {
const connections = [];
for (const [connectionId, connectionInfo] of this.activeConnections) {
connections.push({
id: connectionId,
connected: connectionInfo.connected,
authenticated: connectionInfo.authenticated,
state: connectionInfo.state,
createdAt: connectionInfo.createdAt,
host: this.connectionConfig?.host,
port: this.connectionConfig?.port
});
}
return connections;
}
/**
* Check if SSH is configured and enabled
* @returns {boolean}
*/
isEnabled() {
return this.connectionConfig && this.connectionConfig.enabled;
}
/**
* Get current SSH configuration (without password)
* @returns {Object} Configuration object
*/
getConfig() {
if (!this.connectionConfig) {
return null;
}
return {
host: this.connectionConfig.host,
port: this.connectionConfig.port,
username: this.connectionConfig.username,
enabled: this.connectionConfig.enabled
};
}
}
// Create singleton instance
const sshService = new SshService();
module.exports = {
SshService,
sshService
};