ssh_servers / index.html
measmonysuon's picture
Update index.html
2a09186 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Proxy SSH Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-attach@0.8.0/dist/xterm-addon-attach.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/dist/xterm-addon-fit.min.js"></script>
<style>
.terminal {
padding: 10px;
height: 100%;
}
.connection-status {
transition: all 0.3s ease;
}
.tab-active {
border-bottom: 3px solid #3b82f6;
color: #3b82f6;
font-weight: 600;
}
#terminal-container {
height: calc(100vh - 180px);
}
.password-field {
position: relative;
}
.password-toggle {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #6b7280;
}
.server-card {
transition: all 0.2s ease;
}
.server-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.server-card.active {
border-left: 4px solid #3b82f6;
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-6 max-w-7xl">
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
<!-- Header -->
<div class="bg-indigo-600 text-white px-6 py-4 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fas fa-server text-2xl"></i>
<h1 class="text-2xl font-bold">Multi-Proxy SSH Manager</h1>
</div>
<div id="connection-status" class="connection-status bg-gray-500 text-white px-3 py-1 rounded-full text-sm flex items-center">
<span class="w-3 h-3 rounded-full bg-current mr-2"></span>
<span>Disconnected</span>
</div>
</div>
<!-- Tabs -->
<div class="border-b border-gray-200">
<div class="flex px-6">
<button id="servers-tab" class="tab-active px-4 py-3 text-sm font-medium">Servers</button>
<button id="new-server-tab" class="px-4 py-3 text-sm font-medium text-gray-500 hover:text-gray-700">New Server</button>
<button id="settings-tab" class="px-4 py-3 text-sm font-medium text-gray-500 hover:text-gray-700">Settings</button>
</div>
</div>
<!-- Servers Panel -->
<div id="servers-panel" class="p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-medium text-gray-900">Your Proxy Servers</h2>
<div class="relative">
<input type="text" id="server-search" placeholder="Search servers..." class="pl-8 pr-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
<i class="fas fa-search absolute left-3 top-3 text-gray-400"></i>
</div>
</div>
<div id="servers-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Server cards will be added here dynamically -->
<div class="text-center py-10 text-gray-500" id="no-servers-message">
<i class="fas fa-server text-4xl mb-3"></i>
<p>No servers configured yet</p>
<button id="add-first-server" class="mt-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fas fa-plus mr-2"></i> Add Your First Server
</button>
</div>
</div>
</div>
<!-- New Server Panel -->
<div id="new-server-panel" class="p-6 hidden">
<div class="flex justify-between items-center mb-6">
<h2 class="text-lg font-medium text-gray-900">Add New Proxy Server</h2>
<button id="auto-generate-btn" class="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fas fa-bolt mr-1"></i> Auto Generate
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="mb-4">
<label for="server-name" class="block text-sm font-medium text-gray-700">Server Name</label>
<input type="text" id="server-name" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="My Production Server">
</div>
<div class="space-y-4">
<h3 class="text-md font-medium text-gray-900">SSH Server Details</h3>
<div>
<label for="ssh-host" class="block text-sm font-medium text-gray-700">Host</label>
<input type="text" id="ssh-host" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="example.com">
</div>
<div>
<label for="ssh-port" class="block text-sm font-medium text-gray-700">Port</label>
<input type="number" id="ssh-port" value="22" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
<div>
<label for="ssh-username" class="block text-sm font-medium text-gray-700">Username</label>
<input type="text" id="ssh-username" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="user">
</div>
<div class="password-field">
<label for="ssh-password" class="block text-sm font-medium text-gray-700">Password</label>
<input type="password" id="ssh-password" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="••••••••">
<span class="password-toggle" id="toggle-ssh-password">
<i class="far fa-eye"></i>
</span>
</div>
</div>
</div>
<div>
<h3 class="text-md font-medium text-gray-900 mb-4">Proxy Server Details</h3>
<div class="space-y-4">
<div>
<label for="proxy-type" class="block text-sm font-medium text-gray-700">Proxy Type</label>
<select id="proxy-type" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="http">HTTP/HTTPS</option>
<option value="socks4">SOCKS4</option>
<option value="socks5">SOCKS5</option>
</select>
</div>
<div>
<label for="proxy-host" class="block text-sm font-medium text-gray-700">Proxy Host</label>
<input type="text" id="proxy-host" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="proxy.example.com">
</div>
<div>
<label for="proxy-port" class="block text-sm font-medium text-gray-700">Proxy Port</label>
<input type="number" id="proxy-port" value="8080" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
<div>
<label for="proxy-username" class="block text-sm font-medium text-gray-700">Proxy Username (optional)</label>
<input type="text" id="proxy-username" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
<div class="password-field">
<label for="proxy-password" class="block text-sm font-medium text-gray-700">Proxy Password (optional)</label>
<input type="password" id="proxy-password" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<span class="password-toggle" id="toggle-proxy-password">
<i class="far fa-eye"></i>
</span>
</div>
</div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button id="cancel-add-server" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</button>
<button id="save-server" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fas fa-save mr-2"></i> Save Server
</button>
</div>
</div>
<!-- Settings Panel -->
<div id="settings-panel" class="p-6 hidden">
<h2 class="text-lg font-medium text-gray-900 mb-4">Application Settings</h2>
<div class="space-y-4">
<div>
<label for="terminal-theme" class="block text-sm font-medium text-gray-700">Terminal Theme</label>
<select id="terminal-theme" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="default">Default (Dark)</option>
<option value="light">Light</option>
<option value="solarized-dark">Solarized Dark</option>
<option value="solarized-light">Solarized Light</option>
</select>
</div>
<div>
<label for="terminal-font" class="block text-sm font-medium text-gray-700">Terminal Font Size</label>
<input type="range" id="terminal-font" min="10" max="24" value="14" class="mt-1 block w-full">
<div class="text-sm text-gray-500 mt-1" id="font-size-display">Font Size: 14px</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="bell-sound" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="bell-sound" class="font-medium text-gray-700">Enable Terminal Bell Sound</label>
<p class="text-gray-500">Play sound when terminal bell character is received</p>
</div>
</div>
</div>
<div class="mt-8">
<h3 class="text-md font-medium text-gray-900 mb-2">Data Management</h3>
<div class="space-y-2">
<button id="export-servers" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fas fa-file-export mr-2"></i> Export Server Configurations
</button>
<button id="import-servers" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<i class="fas fa-file-import mr-2"></i> Import Server Configurations
</button>
<input type="file" id="import-file" class="hidden" accept=".json">
</div>
</div>
</div>
<!-- Terminal Container -->
<div id="terminal-container" class="bg-black hidden">
<div id="terminal" class="terminal"></div>
</div>
</div>
<div class="mt-4 text-center text-sm text-gray-500">
<p>Multi-Proxy SSH Manager v1.1 - All connections are client-side only</p>
</div>
</div>
<!-- Server Details Modal -->
<div id="server-details-modal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-indigo-100 sm:mx-0 sm:h-10 sm:w-10">
<i class="fas fa-server text-indigo-600"></i>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">Server Details</h3>
<div class="mt-4">
<div class="grid grid-cols-2 gap-4">
<div>
<p class="text-sm font-medium text-gray-500">Server Name</p>
<p id="modal-server-name" class="mt-1 text-sm text-gray-900"></p>
</div>
<div>
<p class="text-sm font-medium text-gray-500">Status</p>
<p id="modal-server-status" class="mt-1 text-sm text-gray-900"></p>
</div>
<div>
<p class="text-sm font-medium text-gray-500">SSH Host</p>
<p id="modal-ssh-host" class="mt-1 text-sm text-gray-900"></p>
</div>
<div>
<p class="text-sm font-medium text-gray-500">SSH Port</p>
<p id="modal-ssh-port" class="mt-1 text-sm text-gray-900"></p>
</div>
<div>
<p class="text-sm font-medium text-gray-500">Proxy Type</p>
<p id="modal-proxy-type" class="mt-1 text-sm text-gray-900"></p>
</div>
<div>
<p class="text-sm font-medium text-gray-500">Proxy Host</p>
<p id="modal-proxy-host" class="mt-1 text-sm text-gray-900"></p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="button" id="connect-server" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:ml-3 sm:w-auto sm:text-sm">
Connect
</button>
<button type="button" id="edit-server" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Edit
</button>
<button type="button" id="delete-server" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Delete
</button>
<button type="button" id="close-modal" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
Close
</button>
</div>
</div>
</div>
</div>
<script>
// IndexedDB Database Manager
class DatabaseManager {
constructor() {
this.dbName = 'SSHProxyManager';
this.dbVersion = 1;
this.storeName = 'servers';
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = (event) => {
console.error('Database error:', event.target.error);
reject(event.target.error);
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
// Create indexes for searching
store.createIndex('name', 'name', { unique: false });
store.createIndex('sshHost', 'sshHost', { unique: false });
store.createIndex('status', 'status', { unique: false });
}
};
});
}
async getAllServers() {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async saveServer(server) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(server);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async deleteServer(serverId) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(serverId);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async searchServers(query) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const nameIndex = store.index('name');
const request = nameIndex.getAll();
request.onsuccess = () => {
const servers = request.result;
const results = servers.filter(server =>
server.name.toLowerCase().includes(query.toLowerCase()) ||
server.sshHost.toLowerCase().includes(query.toLowerCase())
);
resolve(results);
};
request.onerror = () => reject(request.error);
});
}
async updateServerStatus(serverId, status) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.get(serverId);
request.onsuccess = () => {
const server = request.result;
if (server) {
server.status = status;
server.lastStateChange = new Date().toISOString();
const updateRequest = store.put(server);
updateRequest.onsuccess = () => resolve(server);
updateRequest.onerror = () => reject(updateRequest.error);
} else {
reject(new Error('Server not found'));
}
};
request.onerror = () => reject(request.error);
});
}
}
// Initialize database manager
const dbManager = new DatabaseManager();
let servers = []; // Keep this for compatibility with existing code
// Server management
let currentServerId = null;
let term;
let socket;
let fitAddon;
// Add connection state management
const ConnectionState = {
DISCONNECTED: 'disconnected',
CONNECTING: 'connecting',
CONNECTED: 'connected',
ERROR: 'error'
};
// Add session management
class SSHSession {
constructor(server) {
this.server = server;
this.term = null;
this.socket = null;
this.connectionStartTime = null;
this.lastActivity = null;
}
start() {
this.connectionStartTime = new Date();
this.updateActivity();
// Initialize terminal and connection
}
updateActivity() {
this.lastActivity = new Date();
}
isIdle(timeoutMinutes = 30) {
if (!this.lastActivity) return false;
const idleTime = (new Date() - this.lastActivity) / 1000 / 60;
return idleTime > timeoutMinutes;
}
}
// Add terminal history and command completion
class TerminalHistory {
constructor(maxSize = 100) {
this.history = [];
this.maxSize = maxSize;
this.position = -1;
}
add(command) {
if (command && command.trim()) {
this.history.unshift(command);
if (this.history.length > this.maxSize) {
this.history.pop();
}
}
this.position = -1;
}
previous() {
if (this.position < this.history.length - 1) {
this.position++;
return this.history[this.position];
}
return null;
}
next() {
if (this.position > 0) {
this.position--;
return this.history[this.position];
} else if (this.position === 0) {
this.position = -1;
return '';
}
return null;
}
}
// Initialize the app
document.addEventListener('DOMContentLoaded', async () => {
try {
await dbManager.init();
servers = await dbManager.getAllServers();
renderServers();
setupEventListeners();
} catch (error) {
console.error('Failed to initialize database:', error);
showNotification('Failed to load server configurations', 'red');
}
});
// Tab switching
function setupEventListeners() {
document.getElementById('servers-tab').addEventListener('click', () => {
document.getElementById('servers-panel').classList.remove('hidden');
document.getElementById('new-server-panel').classList.add('hidden');
document.getElementById('settings-panel').classList.add('hidden');
document.getElementById('servers-tab').classList.add('tab-active');
document.getElementById('new-server-tab').classList.remove('tab-active');
document.getElementById('settings-tab').classList.remove('tab-active');
});
document.getElementById('new-server-tab').addEventListener('click', () => {
document.getElementById('servers-panel').classList.add('hidden');
document.getElementById('new-server-panel').classList.remove('hidden');
document.getElementById('settings-panel').classList.add('hidden');
document.getElementById('servers-tab').classList.remove('tab-active');
document.getElementById('new-server-tab').classList.add('tab-active');
document.getElementById('settings-tab').classList.remove('tab-active');
resetForm();
});
document.getElementById('settings-tab').addEventListener('click', () => {
document.getElementById('servers-panel').classList.add('hidden');
document.getElementById('new-server-panel').classList.add('hidden');
document.getElementById('settings-panel').classList.remove('hidden');
document.getElementById('servers-tab').classList.remove('tab-active');
document.getElementById('new-server-tab').classList.remove('tab-active');
document.getElementById('settings-tab').classList.add('tab-active');
});
// Font size slider
document.getElementById('terminal-font').addEventListener('input', (e) => {
const size = e.target.value;
document.getElementById('font-size-display').textContent = `Font Size: ${size}px`;
if (term) {
term.setOption('fontSize', size);
}
});
// Password toggle functionality
function setupPasswordToggle(toggleId, inputId) {
const toggle = document.getElementById(toggleId);
const input = document.getElementById(inputId);
toggle.addEventListener('click', () => {
if (input.type === 'password') {
input.type = 'text';
toggle.innerHTML = '<i class="far fa-eye-slash"></i>';
} else {
input.type = 'password';
toggle.innerHTML = '<i class="far fa-eye"></i>';
}
});
}
setupPasswordToggle('toggle-ssh-password', 'ssh-password');
setupPasswordToggle('toggle-proxy-password', 'proxy-password');
// Terminal theme change
document.getElementById('terminal-theme').addEventListener('change', updateTerminalTheme);
// Add first server button
document.getElementById('add-first-server').addEventListener('click', () => {
document.getElementById('servers-tab').click();
document.getElementById('new-server-tab').click();
});
// Save server button
document.getElementById('save-server').addEventListener('click', saveServer);
// Cancel add server
document.getElementById('cancel-add-server').addEventListener('click', () => {
document.getElementById('servers-tab').click();
});
// Auto generate credentials
document.getElementById('auto-generate-btn').addEventListener('click', autoGenerateCredentials);
// Server search
document.getElementById('server-search').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Modal buttons
const modalButtons = {
'connect-server': connectToServer,
'edit-server': editServer,
'delete-server': deleteServer,
'close-modal': closeModal
};
// Add event listeners to modal buttons
Object.entries(modalButtons).forEach(([id, handler]) => {
const button = document.getElementById(id);
if (button) {
button.addEventListener('click', handler);
}
});
// Add keyboard event listener for modal
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
// Add click event listener for modal backdrop
const modal = document.getElementById('server-details-modal');
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
});
// Data management
document.getElementById('export-servers').addEventListener('click', exportServers);
document.getElementById('import-servers').addEventListener('click', () => {
document.getElementById('import-file').click();
});
document.getElementById('import-file').addEventListener('change', importServers);
// Add accessibility
addAccessibility();
}
// Render server cards
function renderServers() {
const container = document.getElementById('servers-container');
const noServersMessage = document.getElementById('no-servers-message');
if (servers.length === 0) {
noServersMessage.classList.remove('hidden');
container.innerHTML = '';
container.appendChild(noServersMessage);
return;
}
noServersMessage.classList.add('hidden');
container.innerHTML = '';
servers.forEach(server => {
const card = document.createElement('div');
card.className = 'server-card bg-white rounded-lg border border-gray-200 p-4 cursor-pointer';
card.dataset.id = server.id;
card.innerHTML = `
<div class="flex justify-between items-start">
<div>
<h3 class="server-name text-lg font-medium text-gray-900">${server.name}</h3>
<p class="text-sm text-gray-500 mt-1">${server.sshHost}:${server.sshPort}</p>
</div>
<span class="px-2 py-1 text-xs rounded-full ${server.status === 'connected' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}">
${server.status || 'disconnected'}
</span>
</div>
<div class="mt-4 flex justify-between items-center">
<div>
<p class="text-xs text-gray-500">Proxy: ${server.proxyType.toUpperCase()}</p>
<p class="text-xs text-gray-500">${server.proxyHost}:${server.proxyPort}</p>
</div>
<button class="details-btn p-1 text-gray-400 hover:text-gray-500">
<i class="fas fa-ellipsis-h"></i>
</button>
</div>
`;
container.appendChild(card);
// Add click event to card
card.addEventListener('click', (e) => {
if (!e.target.classList.contains('details-btn') && !e.target.closest('.details-btn')) {
showServerDetails(server.id);
}
});
// Add click event to details button
const detailsBtn = card.querySelector('.details-btn');
detailsBtn.addEventListener('click', (e) => {
e.stopPropagation();
showServerDetails(server.id);
});
});
}
// Show server details modal
function showServerDetails(serverId) {
const server = servers.find(s => s.id === serverId);
if (!server) return;
currentServerId = serverId;
// Update modal content
document.getElementById('modal-server-name').textContent = server.name;
document.getElementById('modal-server-status').textContent = server.status || 'disconnected';
document.getElementById('modal-ssh-host').textContent = server.sshHost;
document.getElementById('modal-ssh-port').textContent = server.sshPort;
document.getElementById('modal-proxy-type').textContent = server.proxyType.toUpperCase();
document.getElementById('modal-proxy-host').textContent = `${server.proxyHost}:${server.proxyPort}`;
// Update connect button based on status
const connectBtn = document.getElementById('connect-server');
if (server.status === 'connected') {
connectBtn.innerHTML = '<i class="fas fa-power-off mr-2"></i> Disconnect';
connectBtn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700');
connectBtn.classList.add('bg-red-600', 'hover:bg-red-700');
} else {
connectBtn.innerHTML = '<i class="fas fa-plug mr-2"></i> Connect';
connectBtn.classList.remove('bg-red-600', 'hover:bg-red-700');
connectBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
}
// Show modal
document.getElementById('server-details-modal').classList.remove('hidden');
}
// Close modal
function closeModal() {
document.getElementById('server-details-modal').classList.add('hidden');
currentServerId = null;
}
// Connect to server
function connectToServer() {
if (!currentServerId) return;
const server = servers.find(s => s.id === currentServerId);
if (!server) return;
if (server.status === 'connected') {
// Disconnect
disconnectFromServer(server.id);
} else {
// Connect
const connectBtn = document.getElementById('connect-server');
connectBtn.disabled = true;
connectBtn.innerHTML = '<i class="fas fa-circle-notch fa-spin mr-2"></i> Connecting...';
// Update status
updateConnectionState(server.id, ConnectionState.CONNECTING);
// Simulate connection (in a real app, this would connect to your backend)
setTimeout(() => {
updateConnectionState(server.id, ConnectionState.CONNECTED);
connectBtn.innerHTML = '<i class="fas fa-power-off mr-2"></i> Disconnect';
connectBtn.classList.remove('bg-indigo-600', 'hover:bg-indigo-700');
connectBtn.classList.add('bg-red-600', 'hover:bg-red-700');
connectBtn.disabled = false;
// Initialize terminal if not already done
if (!term) {
initTerminal();
}
// Write welcome message to terminal
term.writeln(`\x1b[32mConnected to ${server.sshHost} through ${server.proxyType.toUpperCase()} proxy\x1b[0m`);
term.writeln('\x1b[33mThis is a simulation - in a real app, this would be a live SSH session\x1b[0m');
term.writeln('');
// Prompt
term.write('$ ');
// Handle user input (simulated)
term.onData(data => {
term.write(data);
if (data === '\r') {
term.write('\r\n$ ');
}
});
// Update connection status in header
const statusElement = document.getElementById('connection-status');
statusElement.className = 'connection-status bg-green-500 text-white px-3 py-1 rounded-full text-sm flex items-center';
statusElement.innerHTML = '<span class="w-3 h-3 rounded-full bg-current mr-2"></span><span>Connected to ' + server.name + '</span>';
// Show terminal
document.getElementById('terminal-container').classList.remove('hidden');
// Close modal after successful connection
closeModal();
}, 1500);
}
}
// Disconnect from server
function disconnectFromServer(serverId) {
const server = servers.find(s => s.id === serverId);
if (!server) return;
updateConnectionState(serverId, ConnectionState.DISCONNECTED);
// Update modal
const connectBtn = document.getElementById('connect-server');
connectBtn.innerHTML = '<i class="fas fa-plug mr-2"></i> Connect';
connectBtn.classList.remove('bg-red-600', 'hover:bg-red-700');
connectBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
// Update connection status in header
const statusElement = document.getElementById('connection-status');
statusElement.className = 'connection-status bg-gray-500 text-white px-3 py-1 rounded-full text-sm flex items-center';
statusElement.innerHTML = '<span class="w-3 h-3 rounded-full bg-current mr-2"></span><span>Disconnected</span>';
// Hide terminal container
document.getElementById('terminal-container').classList.add('hidden');
// In a real app, close the connection here
if (term) {
term.writeln('\r\n\x1b[31mConnection closed\x1b[0m');
term = null;
}
// Close modal
closeModal();
// Show notification
showNotification('Disconnected from server', 'blue');
}
// Edit server
function editServer() {
if (!currentServerId) return;
const server = servers.find(s => s.id === currentServerId);
if (!server) return;
// Fill form with server data
document.getElementById('server-name').value = server.name;
document.getElementById('ssh-host').value = server.sshHost;
document.getElementById('ssh-port').value = server.sshPort;
document.getElementById('ssh-username').value = server.sshUsername;
document.getElementById('ssh-password').value = server.sshPassword || '';
document.getElementById('proxy-type').value = server.proxyType;
document.getElementById('proxy-host').value = server.proxyHost;
document.getElementById('proxy-port').value = server.proxyPort;
document.getElementById('proxy-username').value = server.proxyUsername || '';
document.getElementById('proxy-password').value = server.proxyPassword || '';
// Switch to edit mode
document.getElementById('new-server-tab').click();
const saveBtn = document.getElementById('save-server');
saveBtn.dataset.mode = 'edit';
saveBtn.dataset.id = server.id;
// Close modal
closeModal();
// Show notification
showNotification('Editing server configuration', 'blue');
}
// Delete server
async function deleteServer() {
if (!currentServerId) return;
const server = servers.find(s => s.id === currentServerId);
if (!server) return;
if (confirm(`Are you sure you want to delete the server "${server.name}"?`)) {
try {
if (server.status === 'connected') {
disconnectFromServer(server.id);
}
// Delete from IndexedDB
await dbManager.deleteServer(currentServerId);
// Update local array
servers = servers.filter(s => s.id !== currentServerId);
renderServers();
closeModal();
showNotification('Server configuration deleted', 'green');
} catch (error) {
console.error('Failed to delete server:', error);
showNotification('Failed to delete server configuration', 'red');
}
}
}
// Save server
async function saveServer() {
const mode = this.dataset.mode;
const serverId = this.dataset.id;
const server = {
name: document.getElementById('server-name').value.trim(),
sshHost: document.getElementById('ssh-host').value.trim(),
sshPort: document.getElementById('ssh-port').value.trim(),
sshUsername: document.getElementById('ssh-username').value.trim(),
sshPassword: document.getElementById('ssh-password').value,
proxyType: document.getElementById('proxy-type').value,
proxyHost: document.getElementById('proxy-host').value.trim(),
proxyPort: document.getElementById('proxy-port').value.trim(),
proxyUsername: document.getElementById('proxy-username').value.trim(),
proxyPassword: document.getElementById('proxy-password').value,
status: 'disconnected',
lastConnected: null
};
// Validate required fields
if (!server.name || !server.sshHost || !server.sshUsername || !server.proxyHost) {
showNotification('Please fill in all required fields', 'red');
return;
}
try {
// Encrypt sensitive data
const sensitiveData = {
sshPassword: server.sshPassword,
proxyPassword: server.proxyPassword
};
server.encryptedCredentials = await encryptCredentials(sensitiveData);
delete server.sshPassword;
delete server.proxyPassword;
if (mode === 'edit') {
server.id = serverId;
} else {
server.id = Date.now().toString();
}
// Save to IndexedDB
await dbManager.saveServer(server);
// Update local array
if (mode === 'edit') {
const index = servers.findIndex(s => s.id === serverId);
if (index !== -1) {
servers[index] = server;
}
} else {
servers.push(server);
}
renderServers();
document.getElementById('servers-tab').click();
showNotification(`Server ${mode === 'edit' ? 'updated' : 'added'} successfully`, 'green');
resetForm();
} catch (error) {
console.error('Failed to save server:', error);
showNotification('Failed to save server configuration', 'red');
}
}
// Reset form
function resetForm() {
document.getElementById('server-name').value = '';
document.getElementById('ssh-host').value = '';
document.getElementById('ssh-port').value = '22';
document.getElementById('ssh-username').value = '';
document.getElementById('ssh-password').value = '';
document.getElementById('proxy-type').value = 'http';
document.getElementById('proxy-host').value = '';
document.getElementById('proxy-port').value = '8080';
document.getElementById('proxy-username').value = '';
document.getElementById('proxy-password').value = '';
// Remove edit mode if exists
const saveBtn = document.getElementById('save-server');
if (saveBtn.dataset.mode) {
delete saveBtn.dataset.mode;
delete saveBtn.dataset.id;
}
}
// Auto generate credentials
function autoGenerateCredentials() {
// Generate random username (adjective + noun)
const adjectives = ['happy', 'quick', 'silent', 'clever', 'brave', 'gentle', 'jolly', 'lucky', 'merry', 'proud'];
const nouns = ['panda', 'tiger', 'eagle', 'dolphin', 'koala', 'raven', 'fox', 'wolf', 'bear', 'owl'];
const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
const randomNumber = Math.floor(Math.random() * 100);
const username = `${randomAdjective}_${randomNoun}${randomNumber}`;
// Generate random password (12 characters)
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < 12; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
// Set the values
document.getElementById('server-name').value = `${randomAdjective} ${randomNoun} server`;
document.getElementById('ssh-host').value = 'demo.webssh-proxy.com';
document.getElementById('ssh-port').value = '22';
document.getElementById('ssh-username').value = username;
document.getElementById('ssh-password').value = password;
document.getElementById('proxy-host').value = 'proxy.demo.webssh-proxy.com';
// Show notification
showNotification('Demo credentials generated! These will work for 1 hour.', 'blue');
}
// Save servers to localStorage
function saveServers() {
localStorage.setItem('sshProxyServers', JSON.stringify(servers));
}
// Update server status
async function updateConnectionState(serverId, state, error = null) {
try {
const updatedServer = await dbManager.updateServerStatus(serverId, state);
const index = servers.findIndex(s => s.id === serverId);
if (index !== -1) {
servers[index] = updatedServer;
}
renderServers();
updateUIForConnectionState(state, error);
} catch (error) {
console.error('Failed to update server status:', error);
showNotification('Failed to update server status', 'red');
}
}
// Export servers
async function exportServers() {
try {
const allServers = await dbManager.getAllServers();
const dataStr = JSON.stringify(allServers, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportName = 'ssh-proxy-servers-' + new Date().toISOString().slice(0, 10) + '.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportName);
linkElement.click();
showNotification('Server configurations exported successfully', 'green');
} catch (error) {
console.error('Failed to export servers:', error);
showNotification('Failed to export server configurations', 'red');
}
}
// Import servers
async function importServers(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
const importedServers = JSON.parse(e.target.result);
if (Array.isArray(importedServers) && importedServers.length > 0) {
// Save each server to IndexedDB
for (const server of importedServers) {
await dbManager.saveServer(server);
}
// Update local array
servers = await dbManager.getAllServers();
renderServers();
showNotification(`Imported ${importedServers.length} server configuration(s)`, 'green');
} else {
showNotification('No valid server configurations found', 'yellow');
}
} catch (error) {
console.error('Failed to import servers:', error);
showNotification('Failed to import server configurations', 'red');
}
event.target.value = '';
};
reader.readAsText(file);
}
// Show notification
function showNotification(message, color) {
const colors = {
green: 'bg-green-500',
blue: 'bg-blue-500',
yellow: 'bg-yellow-500',
red: 'bg-red-500'
};
const notification = document.createElement('div');
notification.className = `fixed bottom-4 right-4 ${colors[color] || 'bg-gray-500'} text-white px-4 py-2 rounded-md shadow-lg flex items-center animate-pulse`;
notification.innerHTML = `
<i class="fas fa-${color === 'green' ? 'check-circle' : color === 'red' ? 'exclamation-circle' : 'info-circle'} mr-2"></i>
<span>${message}</span>
`;
document.body.appendChild(notification);
// Remove after 3 seconds
setTimeout(() => {
notification.classList.add('opacity-0', 'transition-opacity', 'duration-300');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Terminal initialization
function initTerminal() {
// Create terminal
term = new Terminal({
cursorBlink: true,
fontSize: parseInt(document.getElementById('terminal-font').value),
theme: {
background: '#000000',
foreground: '#ffffff'
}
});
// Load theme
updateTerminalTheme();
// Addons
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
// Open terminal in container
const terminalContainer = document.getElementById('terminal-container');
terminalContainer.classList.remove('hidden');
term.open(document.getElementById('terminal'));
// Fit terminal to container
fitAddon.fit();
// Handle window resize
window.addEventListener('resize', () => {
fitAddon.fit();
});
}
function updateTerminalTheme() {
if (!term) return;
const theme = document.getElementById('terminal-theme').value;
let themeConfig;
switch(theme) {
case 'light':
themeConfig = {
background: '#ffffff',
foreground: '#000000',
cursor: '#000000',
selection: 'rgba(0, 0, 0, 0.3)'
};
break;
case 'solarized-dark':
themeConfig = {
background: '#002b36',
foreground: '#839496',
cursor: '#839496',
selection: 'rgba(0, 43, 54, 0.5)'
};
break;
case 'solarized-light':
themeConfig = {
background: '#fdf6e3',
foreground: '#657b83',
cursor: '#657b83',
selection: 'rgba(253, 246, 227, 0.5)'
};
break;
default: // default dark
themeConfig = {
background: '#000000',
foreground: '#ffffff',
cursor: '#ffffff',
selection: 'rgba(255, 255, 255, 0.3)'
};
}
term.setOption('theme', themeConfig);
}
// Enhanced encryption using WebCrypto API
async function encryptCredentials(data) {
const encoder = new TextEncoder();
const key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encoded = encoder.encode(JSON.stringify(data));
const encrypted = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv
},
key,
encoded
);
return {
encrypted: Array.from(new Uint8Array(encrypted)),
iv: Array.from(iv),
key: await exportKey(key)
};
}
// Enhanced error handling
function handleConnectionError(error, serverId) {
const errorTypes = {
NETWORK_ERROR: 'network',
AUTH_ERROR: 'authentication',
PROXY_ERROR: 'proxy',
TIMEOUT: 'timeout'
};
const errorMessages = {
[errorTypes.NETWORK_ERROR]: 'Network connection failed',
[errorTypes.AUTH_ERROR]: 'Authentication failed',
[errorTypes.PROXY_ERROR]: 'Proxy connection failed',
[errorTypes.TIMEOUT]: 'Connection timed out'
};
updateConnectionState(serverId, ConnectionState.ERROR, {
type: error.type,
message: errorMessages[error.type] || 'Unknown error occurred',
timestamp: new Date().toISOString(),
details: error.details
});
showNotification(errorMessages[error.type] || 'Connection error', 'red');
}
// Add debouncing for search
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const debouncedSearch = debounce(async (searchTerm) => {
try {
const results = await dbManager.searchServers(searchTerm);
const serverCards = document.querySelectorAll('.server-card');
serverCards.forEach(card => {
const serverId = card.dataset.id;
const found = results.some(server => server.id === serverId);
card.classList.toggle('hidden', !found);
});
} catch (error) {
console.error('Failed to search servers:', error);
showNotification('Failed to search servers', 'red');
}
}, 300);
// Add accessibility
function addAccessibility() {
// Add ARIA labels
const serverCards = document.querySelectorAll('.server-card');
serverCards.forEach(card => {
card.setAttribute('role', 'button');
card.setAttribute('tabindex', '0');
card.setAttribute('aria-label', `Connect to ${card.querySelector('.server-name').textContent}`);
// Add keyboard navigation
card.addEventListener('keypress', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
card.click();
}
});
});
}
// Add input validation
const ValidationRules = {
serverName: {
minLength: 3,
maxLength: 50,
pattern: /^[a-zA-Z0-9\s-_]+$/
},
host: {
pattern: /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
},
port: {
min: 1,
max: 65535
}
};
function validateServerData(server) {
const errors = [];
if (!ValidationRules.serverName.pattern.test(server.name)) {
errors.push('Invalid server name format');
}
if (!ValidationRules.host.pattern.test(server.sshHost)) {
errors.push('Invalid SSH host format');
}
const port = parseInt(server.sshPort);
if (isNaN(port) || port < ValidationRules.port.min || port > ValidationRules.port.max) {
errors.push('Invalid port number');
}
return errors;
}
</script>
</body>
</html>