{/* Search bar */}
-
+
setSearch(e.target.value)}
@@ -890,7 +1079,7 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
)}
-
+
0 ? sortedFolders : undefined}>
{sortedFolders.map((folder, idx) => (
@@ -921,6 +1110,65 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
+ {/* Tools Button at the very bottom */}
+
+
+
+
+
+
+
+ Tools
+
+
+
+
+ Run multiwindow commands
+
+
+
+
+
+
+
+
@@ -1137,7 +1385,7 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
{
const file = e.target.files?.[0];
field.onChange(file || null);
@@ -1152,6 +1400,70 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
)}
/>
+ (
+
+ Key Password (if protected)
+
+
+
+
+
+ )}
+ />
+ (
+
+ Key Type
+
+
+
+ {editKeyTypeDropdownOpen && (
+
+
+ {keyTypeOptions.map(opt => (
+
+ ))}
+
+
+ )}
+
+
+
+
+ )}
+ />
)}
@@ -1270,7 +1582,7 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
{
const file = e.target.files?.[0];
field.onChange(file || null);
@@ -1285,6 +1597,70 @@ export function SSHSidebar({ onSelectView, onAddHostSubmit, onHostConnect }: Sid
)}
/>
+ (
+
+ Key Password (if protected)
+
+
+
+
+
+ )}
+ />
+ (
+
+ Key Type
+
+
+
+ {keyTypeDropdownOpenAuth && (
+
+
+ {keyTypeOptions.map(opt => (
+
+ ))}
+
+
+ )}
+
+
+
+
+ )}
+ />
)}
diff --git a/src/apps/SSH/SSHTerminal.tsx b/src/apps/SSH/SSHTerminal.tsx
index 899b6e6b..c8c60414 100644
--- a/src/apps/SSH/SSHTerminal.tsx
+++ b/src/apps/SSH/SSHTerminal.tsx
@@ -31,6 +31,11 @@ export const SSHTerminal = forwardRef(function SSHTermina
if (fitAddonRef.current) {
fitAddonRef.current.fit();
}
+ },
+ sendInput: (data: string) => {
+ if (webSocketRef.current && webSocketRef.current.readyState === 1) {
+ webSocketRef.current.send(JSON.stringify({ type: 'input', data }));
+ }
}
}), []);
diff --git a/src/apps/Template/TemplateSidebar.tsx b/src/apps/Template/TemplateSidebar.tsx
index 12441b58..f5a1889b 100644
--- a/src/apps/Template/TemplateSidebar.tsx
+++ b/src/apps/Template/TemplateSidebar.tsx
@@ -21,6 +21,7 @@ import {
import {
Separator,
} from "@/components/ui/separator.tsx"
+import Icon from "../../../public/icon.svg";
interface SidebarProps {
onSelectView: (view: string) => void;
@@ -32,8 +33,9 @@ export function TemplateSidebar({ onSelectView }: SidebarProps): React.ReactElem
-
- Termix / Template
+
+
+ - Termix / Template
diff --git a/src/apps/Tools/ToolsSidebar.tsx b/src/apps/Tools/ToolsSidebar.tsx
index 0172ed86..6fe480d7 100644
--- a/src/apps/Tools/ToolsSidebar.tsx
+++ b/src/apps/Tools/ToolsSidebar.tsx
@@ -21,6 +21,7 @@ import {
import {
Separator,
} from "@/components/ui/separator.tsx"
+import Icon from "../../../public/icon.svg";
interface SidebarProps {
onSelectView: (view: string) => void;
@@ -32,8 +33,9 @@ export function ToolsSidebar({ onSelectView }: SidebarProps): React.ReactElement
-
- Termix / Tools
+
+
+ - Termix / Tools
diff --git a/src/backend/db/db/index.ts b/src/backend/db/db/index.ts
index affe5e98..e00e510d 100644
--- a/src/backend/db/db/index.ts
+++ b/src/backend/db/db/index.ts
@@ -58,6 +58,8 @@ CREATE TABLE IF NOT EXISTS ssh_data (
password TEXT,
auth_method TEXT,
key TEXT,
+ key_password TEXT,
+ key_type TEXT,
save_auth_method INTEGER,
is_pinned INTEGER,
FOREIGN KEY(user_id) REFERENCES users(id)
diff --git a/src/backend/db/db/schema.ts b/src/backend/db/db/schema.ts
index 1ec9b8d7..3768525f 100644
--- a/src/backend/db/db/schema.ts
+++ b/src/backend/db/db/schema.ts
@@ -18,7 +18,9 @@ export const sshData = sqliteTable('ssh_data', {
username: text('username'),
password: text('password'),
authMethod: text('auth_method'),
- key: text('key', { length: 2048 }),
+ key: text('key', { length: 8192 }), // Increased for larger keys
+ keyPassword: text('key_password'), // Password for protected keys
+ keyType: text('key_type'), // Type of SSH key (RSA, ED25519, etc.)
saveAuthMethod: integer('save_auth_method', { mode: 'boolean' }),
isPinned: integer('is_pinned', { mode: 'boolean' }),
});
diff --git a/src/backend/db/routes/ssh.ts b/src/backend/db/routes/ssh.ts
index ea10cc98..f287f88d 100644
--- a/src/backend/db/routes/ssh.ts
+++ b/src/backend/db/routes/ssh.ts
@@ -69,7 +69,7 @@ function authenticateJWT(req: Request, res: Response, next: NextFunction) {
// Route: Create SSH data (requires JWT)
// POST /ssh/host
router.post('/host', authenticateJWT, async (req: Request, res: Response) => {
- const { name, folder, tags, ip, port, username, password, authMethod, key, saveAuthMethod, isPinned } = req.body;
+ const { name, folder, tags, ip, port, username, password, authMethod, key, keyPassword, keyType, saveAuthMethod, isPinned } = req.body;
const userId = (req as any).userId;
if (!isNonEmptyString(userId) || !isNonEmptyString(ip) || !isValidPort(port)) {
logger.warn('Invalid SSH data input');
@@ -93,13 +93,19 @@ router.post('/host', authenticateJWT, async (req: Request, res: Response) => {
if (authMethod === 'password') {
sshDataObj.password = password;
sshDataObj.key = null;
+ sshDataObj.keyPassword = null;
+ sshDataObj.keyType = null;
} else if (authMethod === 'key') {
sshDataObj.key = key;
+ sshDataObj.keyPassword = keyPassword;
+ sshDataObj.keyType = keyType;
sshDataObj.password = null;
}
} else {
sshDataObj.password = null;
sshDataObj.key = null;
+ sshDataObj.keyPassword = null;
+ sshDataObj.keyType = null;
}
try {
@@ -114,7 +120,7 @@ router.post('/host', authenticateJWT, async (req: Request, res: Response) => {
// Route: Update SSH data (requires JWT)
// PUT /ssh/host/:id
router.put('/host/:id', authenticateJWT, async (req: Request, res: Response) => {
- const { name, folder, tags, ip, port, username, password, authMethod, key, saveAuthMethod, isPinned } = req.body;
+ const { name, folder, tags, ip, port, username, password, authMethod, key, keyPassword, keyType, saveAuthMethod, isPinned } = req.body;
const { id } = req.params;
const userId = (req as any).userId;
@@ -139,13 +145,19 @@ router.put('/host/:id', authenticateJWT, async (req: Request, res: Response) =>
if (authMethod === 'password') {
sshDataObj.password = password;
sshDataObj.key = null;
+ sshDataObj.keyPassword = null;
+ sshDataObj.keyType = null;
} else if (authMethod === 'key') {
sshDataObj.key = key;
+ sshDataObj.keyPassword = keyPassword;
+ sshDataObj.keyType = keyType;
sshDataObj.password = null;
}
} else {
sshDataObj.password = null;
sshDataObj.key = null;
+ sshDataObj.keyPassword = null;
+ sshDataObj.keyType = null;
}
try {
diff --git a/src/backend/ssh/ssh.ts b/src/backend/ssh/ssh.ts
index 5044caa5..a24a9e74 100644
--- a/src/backend/ssh/ssh.ts
+++ b/src/backend/ssh/ssh.ts
@@ -81,11 +81,13 @@ wss.on('connection', (ws: WebSocket) => {
username: string;
password?: string;
key?: string;
+ keyPassword?: string;
+ keyType?: string;
authMethod?: string;
};
}) {
const { cols, rows, hostConfig } = data;
- const { ip, port, username, password, key, authMethod } = hostConfig;
+ const { ip, port, username, password, key, keyPassword, keyType, authMethod } = hostConfig;
if (!username || typeof username !== 'string' || username.trim() === '') {
logger.error('Invalid username provided');
@@ -147,7 +149,23 @@ wss.on('connection', (ws: WebSocket) => {
sshConn.on('error', (err: Error) => {
logger.error('SSH connection error: ' + err.message);
- ws.send(JSON.stringify({ type: 'error', message: 'SSH error: ' + err.message }));
+
+ let errorMessage = 'SSH error: ' + err.message;
+ if (err.message.includes('No matching key exchange algorithm')) {
+ errorMessage = 'SSH error: No compatible key exchange algorithm found. This may be due to an older SSH server or network device.';
+ } else if (err.message.includes('No matching cipher')) {
+ errorMessage = 'SSH error: No compatible cipher found. This may be due to an older SSH server or network device.';
+ } else if (err.message.includes('No matching MAC')) {
+ errorMessage = 'SSH error: No compatible MAC algorithm found. This may be due to an older SSH server or network device.';
+ } else if (err.message.includes('ENOTFOUND') || err.message.includes('ENOENT')) {
+ errorMessage = 'SSH error: Could not resolve hostname or connect to server.';
+ } else if (err.message.includes('ECONNREFUSED')) {
+ errorMessage = 'SSH error: Connection refused. The server may not be running or the port may be incorrect.';
+ } else if (err.message.includes('ETIMEDOUT')) {
+ errorMessage = 'SSH error: Connection timed out. Check your network connection and server availability.';
+ }
+
+ ws.send(JSON.stringify({ type: 'error', message: errorMessage }));
cleanupSSH();
});
@@ -162,12 +180,51 @@ wss.on('connection', (ws: WebSocket) => {
keepaliveInterval: 5000,
keepaliveCountMax: 10,
readyTimeout: 10000,
+
+ algorithms: {
+ kex: [
+ 'diffie-hellman-group14-sha256',
+ 'diffie-hellman-group14-sha1',
+ 'diffie-hellman-group1-sha1',
+ 'diffie-hellman-group-exchange-sha256',
+ 'diffie-hellman-group-exchange-sha1',
+ 'ecdh-sha2-nistp256',
+ 'ecdh-sha2-nistp384',
+ 'ecdh-sha2-nistp521'
+ ],
+ cipher: [
+ 'aes128-ctr',
+ 'aes192-ctr',
+ 'aes256-ctr',
+ 'aes128-gcm@openssh.com',
+ 'aes256-gcm@openssh.com',
+ 'aes128-cbc',
+ 'aes192-cbc',
+ 'aes256-cbc',
+ '3des-cbc'
+ ],
+ hmac: [
+ 'hmac-sha2-256',
+ 'hmac-sha2-512',
+ 'hmac-sha1',
+ 'hmac-md5'
+ ],
+ compress: [
+ 'none',
+ 'zlib@openssh.com',
+ 'zlib'
+ ]
+ }
};
if (authMethod === 'key' && key) {
connectConfig.privateKey = key;
+ if (keyPassword) {
+ connectConfig.passphrase = keyPassword;
+ }
} else {
connectConfig.password = password;
}
+
sshConn.connect(connectConfig);
}
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 00000000..51f466ec
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,183 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectGroup({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectValue({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectTrigger({
+ className,
+ size = "default",
+ children,
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "popper",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}