From 4896b71b01a64cd68a9a0197e0e33281cfdfd549 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Mon, 12 Jan 2026 00:21:59 -0600 Subject: [PATCH 1/2] fix: remove top tech --- README.md | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 9ddf04d6..0a1fbe7b 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,6 @@ Achieved on September 1st, 2025

-#### Top Technologies - -[![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) -[![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) -[![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) -[![Vite Badge](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&labelColor=black&logo=vite&logoColor=646CFF)](#) -[![Tailwind CSS Badge](https://img.shields.io/badge/-TailwindCSS-38B2AC?style=flat-square&labelColor=black&logo=tailwindcss&logoColor=38B2AC)](#) -[![Docker Badge](https://img.shields.io/badge/-Docker-2496ED?style=flat-square&labelColor=black&logo=docker&logoColor=2496ED)](#) -[![SQLite Badge](https://img.shields.io/badge/-SQLite-003B57?style=flat-square&labelColor=black&logo=sqlite&logoColor=003B57)](#) -[![Radix UI Badge](https://img.shields.io/badge/-Radix%20UI-161618?style=flat-square&labelColor=black&logo=radixui&logoColor=161618)](#) -

@@ -45,7 +34,7 @@ If you would like, you can support the project here!\ Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal -access, SSH tunneling capabilities, remote file management, and many other tools. Termix is the perfect +access, SSH tunneling capabilities, and remote file management, with many more tools to come. Termix is the perfect free and self-hosted alternative to Termius available for all platforms. # Features @@ -53,22 +42,20 @@ free and self-hosted alternative to Termius available for all platforms. - **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components - **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring - **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly -- **Docker Management** - Start, stop, pause, remove containers. View container stats. Control container using docker exec terminal. It was not made to replace Portainer or Dockge but rather to simply manage your containers compared to creating them. - **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders, and easily save reusable login info while being able to automate the deployment of SSH keys - **Server Stats** - View CPU, memory, and disk usage along with network, uptime, and system information on any SSH server - **Dashboard** - View server information at a glance on your dashboard -- **RBAC** - Create roles and share hosts across users/roles - **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together. - **Database Encryption** - Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more. - **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data - **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects -- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between dark or light mode based UI. -- **Languages** - Built-in support ~30 languages (bulk translated via Google Translate, results may vary ofc) +- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn +- **Languages** - Built-in support for English, Chinese, German, and Portuguese - **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS), and dedicated mobile/tablet app for iOS and Android. - **SSH Tools** - Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals. - **Command History** - Auto-complete and view previously ran SSH commands - **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard -- **SSH Feature Rich** - Supports jump hosts, warpgate, TOTP based connections, SOCKS5, password autofill, etc. +- **SSH Feature Rich** - Supports jump hosts, warpgate, TOTP based connections, etc. # Planned Features @@ -126,7 +113,7 @@ If you need help or want to request a feature with Termix, visit the [Issues](ht Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel, however, response times may be longer. -# Screenshots +# Show-off

Termix Demo 1 @@ -145,12 +132,6 @@ channel, however, response times may be longer.

Termix Demo 7 - Termix Demo 8 -

- -

- Termix Demo 9 - Termix Demo 110

@@ -158,7 +139,7 @@ channel, however, response times may be longer. Your browser does not support the video tag.

-Some videos and images may be out of date or may not perfectly showcase features. +Videos and images may be out of date. # License -- 2.49.1 From 2383f723b3e1e1d984e7016aafcefe5b5bb36c34 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Mon, 12 Jan 2026 14:23:03 +0800 Subject: [PATCH 2/2] fix: use SFTP readdir for file listing to support non-Linux systems The file manager now uses SFTP readdir as the primary method for listing files, with ls -la as a fallback. This enables compatibility with MikroTik RouterOS and other non-Linux systems that don't have standard shell commands. Fixes #317 --- src/backend/ssh/file-manager.ts | 307 ++++++++++++++++++++++++-------- 1 file changed, 229 insertions(+), 78 deletions(-) diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 97f6a88b..bcfa80f6 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -44,6 +44,58 @@ function isExecutableFile(permissions: string, fileName: string): boolean { ); } +function modeToPermissions(mode: number): string { + const S_IFDIR = 0o040000; + const S_IFLNK = 0o120000; + const S_IFMT = 0o170000; + + const type = mode & S_IFMT; + const prefix = type === S_IFDIR ? "d" : type === S_IFLNK ? "l" : "-"; + + const perms = [ + mode & 0o400 ? "r" : "-", + mode & 0o200 ? "w" : "-", + mode & 0o100 ? "x" : "-", + mode & 0o040 ? "r" : "-", + mode & 0o020 ? "w" : "-", + mode & 0o010 ? "x" : "-", + mode & 0o004 ? "r" : "-", + mode & 0o002 ? "w" : "-", + mode & 0o001 ? "x" : "-", + ].join(""); + + return prefix + perms; +} + +function formatMtime(mtime: number): string { + const date = new Date(mtime * 1000); + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + const month = months[date.getMonth()]; + const day = date.getDate().toString().padStart(2, " "); + const now = new Date(); + const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000); + + if (date > sixMonthsAgo) { + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + return `${month} ${day} ${hours}:${minutes}`; + } + return `${month} ${day} ${date.getFullYear()}`; +} + const app = express(); app.use( @@ -1152,88 +1204,187 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { sshConn.lastActive = Date.now(); sshConn.activeOperations++; - const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); - sshConn.client.exec(`command ls -la '${escapedPath}'`, (err, stream) => { - if (err) { - sshConn.activeOperations--; - fileLogger.error("SSH listFiles error:", err); - return res.status(500).json({ error: err.message }); - } - - let data = ""; - let errorData = ""; - - stream.on("data", (chunk: Buffer) => { - data += chunk.toString(); - }); - - stream.stderr.on("data", (chunk: Buffer) => { - errorData += chunk.toString(); - }); - - stream.on("close", (code) => { - sshConn.activeOperations--; - if (code !== 0) { - fileLogger.error( - `SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, - ); - return res.status(500).json({ error: `Command failed: ${errorData}` }); - } - - const lines = data.split("\n").filter((line) => line.trim()); - const files = []; - - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - const parts = line.split(/\s+/); - if (parts.length >= 9) { - const permissions = parts[0]; - const owner = parts[2]; - const group = parts[3]; - const size = parseInt(parts[4], 10); - - let dateStr = ""; - const nameStartIndex = 8; - - if (parts[5] && parts[6] && parts[7]) { - dateStr = `${parts[5]} ${parts[6]} ${parts[7]}`; - } - - const name = parts.slice(nameStartIndex).join(" "); - const isDirectory = permissions.startsWith("d"); - const isLink = permissions.startsWith("l"); - - if (name === "." || name === "..") continue; - - let actualName = name; - let linkTarget = undefined; - if (isLink && name.includes(" -> ")) { - const linkParts = name.split(" -> "); - actualName = linkParts[0]; - linkTarget = linkParts[1]; - } - - files.push({ - name: actualName, - type: isDirectory ? "directory" : isLink ? "link" : "file", - size: isDirectory ? undefined : size, - modified: dateStr, - permissions, - owner, - group, - linkTarget, - path: `${sshPath.endsWith("/") ? sshPath : sshPath + "/"}${actualName}`, - executable: - !isDirectory && !isLink - ? isExecutableFile(permissions, actualName) - : false, - }); + const trySFTP = () => { + try { + sshConn.client.sftp((err, sftp) => { + if (err) { + fileLogger.warn( + `SFTP failed for listFiles, trying fallback: ${err.message}`, + ); + tryFallbackMethod(); + return; } + + sftp.readdir(sshPath, (readdirErr, list) => { + if (readdirErr) { + fileLogger.warn( + `SFTP readdir failed, trying fallback: ${readdirErr.message}`, + ); + tryFallbackMethod(); + return; + } + + const symlinks: Array<{ index: number; path: string }> = []; + const files: Array<{ + name: string; + type: string; + size: number | undefined; + modified: string; + permissions: string; + owner: string; + group: string; + linkTarget: string | undefined; + path: string; + executable: boolean; + }> = []; + + for (const entry of list) { + if (entry.filename === "." || entry.filename === "..") continue; + + const attrs = entry.attrs; + const permissions = modeToPermissions(attrs.mode); + const isDirectory = attrs.isDirectory(); + const isLink = attrs.isSymbolicLink(); + + const fileEntry = { + name: entry.filename, + type: isDirectory ? "directory" : isLink ? "link" : "file", + size: isDirectory ? undefined : attrs.size, + modified: formatMtime(attrs.mtime), + permissions, + owner: String(attrs.uid), + group: String(attrs.gid), + linkTarget: undefined as string | undefined, + path: `${sshPath.endsWith("/") ? sshPath : sshPath + "/"}${entry.filename}`, + executable: + !isDirectory && !isLink + ? isExecutableFile(permissions, entry.filename) + : false, + }; + + if (isLink) { + symlinks.push({ index: files.length, path: fileEntry.path }); + } + + files.push(fileEntry); + } + + if (symlinks.length === 0) { + sshConn.activeOperations--; + return res.json({ files, path: sshPath }); + } + + let resolved = 0; + for (const link of symlinks) { + sftp.readlink(link.path, (linkErr, target) => { + resolved++; + if (!linkErr && target) { + files[link.index].linkTarget = target; + } + if (resolved === symlinks.length) { + sshConn.activeOperations--; + res.json({ files, path: sshPath }); + } + }); + } + }); + }); + } catch (sftpErr: unknown) { + const errMsg = + sftpErr instanceof Error ? sftpErr.message : "Unknown error"; + fileLogger.warn(`SFTP connection error, trying fallback: ${errMsg}`); + tryFallbackMethod(); + } + }; + + const tryFallbackMethod = () => { + const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); + sshConn.client.exec(`command ls -la '${escapedPath}'`, (err, stream) => { + if (err) { + sshConn.activeOperations--; + fileLogger.error("SSH listFiles error:", err); + return res.status(500).json({ error: err.message }); } - res.json({ files, path: sshPath }); + let data = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + data += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); + + stream.on("close", (code) => { + sshConn.activeOperations--; + if (code !== 0) { + fileLogger.error( + `SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + return res + .status(500) + .json({ error: `Command failed: ${errorData}` }); + } + + const lines = data.split("\n").filter((line) => line.trim()); + const files = []; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(/\s+/); + if (parts.length >= 9) { + const permissions = parts[0]; + const owner = parts[2]; + const group = parts[3]; + const size = parseInt(parts[4], 10); + + let dateStr = ""; + const nameStartIndex = 8; + + if (parts[5] && parts[6] && parts[7]) { + dateStr = `${parts[5]} ${parts[6]} ${parts[7]}`; + } + + const name = parts.slice(nameStartIndex).join(" "); + const isDirectory = permissions.startsWith("d"); + const isLink = permissions.startsWith("l"); + + if (name === "." || name === "..") continue; + + let actualName = name; + let linkTarget = undefined; + if (isLink && name.includes(" -> ")) { + const linkParts = name.split(" -> "); + actualName = linkParts[0]; + linkTarget = linkParts[1]; + } + + files.push({ + name: actualName, + type: isDirectory ? "directory" : isLink ? "link" : "file", + size: isDirectory ? undefined : size, + modified: dateStr, + permissions, + owner, + group, + linkTarget, + path: `${sshPath.endsWith("/") ? sshPath : sshPath + "/"}${actualName}`, + executable: + !isDirectory && !isLink + ? isExecutableFile(permissions, actualName) + : false, + }); + } + } + + res.json({ files, path: sshPath }); + }); }); - }); + }; + + trySFTP(); }); app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => { -- 2.49.1