diff --git a/build.sh b/build.sh index f2c316a..fd2c7e3 100755 --- a/build.sh +++ b/build.sh @@ -6,6 +6,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" +source "$SCRIPT_DIR/scripts/build-common.sh" # Colours for output RED='\033[0;31m' @@ -18,7 +19,7 @@ warn() { echo -e "${YELLOW}[nexus]${NC} $1"; } err() { echo -e "${RED}[nexus]${NC} $1" >&2; } MIN_GO_VERSION="1.25.0" -WAILS_VERSION="$(awk '$1 == "github.com/wailsapp/wails/v2" { print $2; exit }' go.mod)" +WAILS_VERSION="$(nexus_wails_version go.mod)" WAILS_BIN="" WAILS_USE_GO_RUN=0 @@ -118,28 +119,15 @@ check_linux_deps() { } build_tags() { - if command -v pkg-config >/dev/null 2>&1 \ - && pkg-config --exists webkit2gtk-4.1 \ - && ! pkg-config --exists webkit2gtk-4.0; then - echo "webkit2_41" - fi + nexus_build_tags } build_version() { - local tag - local commit - tag="$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")" - commit="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")" - echo "${tag}-${commit}" + nexus_build_version } build_ldflags() { - local version="$1" - local flags="-s -w -X github.com/andydixon/nexus/internal/api.appVersion=${version}" - if [ -n "${NEXUS_UPDATE_URL:-}" ]; then - flags="${flags} -X github.com/andydixon/nexus/internal/api.appUpdateURL=${NEXUS_UPDATE_URL}" - fi - echo "${flags}" + nexus_build_ldflags "$1" } # Check prerequisites @@ -213,6 +201,7 @@ run_tests() { check_go log "Running tests…" go test ./... -v -count=1 + (cd frontend && npm test) } # Clean build artefacts diff --git a/docs/overview.md b/docs/overview.md index 8bd9696..0868ad1 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,26 +1,262 @@ # Nexus Terminal Overview -Nexus Terminal is a desktop client for managing SSH, Telnet, RAW TCP, and GitHub sessions in one place. It is designed for people who work across multiple servers, environments, or repositories and want a fast, focused workflow with strong credential handling. +Nexus Terminal is a desktop connection workspace for people who manage servers, network services, remote desktops, and repositories every day. It brings SSH, Telnet, RAW TCP, VNC, GitHub, and local shell workflows into one focused app with secure credential handling, multi-session organisation, file transfer, automation, and recording built in. -## What it does +## Product positioning -- Connect to SSH, Telnet, and RAW TCP endpoints with saved profiles or quick connect. -- Manage multiple live sessions with tabs, split panes, and broadcast input. -- Browse and move files on SSH hosts with a built-in SFTP browser. -- Record sessions for later playback. -- Store credentials securely with OS keychain support and an encrypted vault fallback. +Nexus is more than an SSH client. It is a single command centre for remote access, terminal operations, file movement, raw socket inspection, GitHub repository visibility, and repeatable operational workflows. -## Highlights +Use Nexus when you need to: -- Multi-session tab bar with pinning, reordering, and split panes. -- SFTP browser with drag-and-drop uploads and a directory picker. -- Clipboard-aware terminal with search, snippets, and right-click actions. -- Auto-reconnect, login scripts, and SSH extras such as jump hosts and port forwarding. -- GitHub view for browsing repositories by organisation. +- Connect quickly to many systems without re-entering credentials. +- Keep multiple live sessions organised with tabs, pinned tabs, split panes, and status indicators. +- Move files over SSH without leaving the terminal workspace. +- Work safely with host key verification, encrypted secrets, TOTP support, and audit logging. +- Reuse commands, automate login flows, record sessions, and react to terminal output. +- Support mixed estates that include SSH servers, Telnet devices, raw TCP services, VNC desktops, GitHub repositories, and local shells. + +## Main feature groups + +### Multi-protocol connections + +- **SSH** for secure remote terminal access. +- **Telnet** for legacy devices and services. +- **RAW TCP** for direct socket interaction, protocol testing, and device troubleshooting. +- **VNC** for remote desktop access. +- **GitHub** for repository browsing and download workflows. +- **Local terminal** for opening a shell on the local machine. +- **Quick connect** for one-off sessions without saving a profile. +- **Saved profiles** for repeat connections. + +### Profile management + +- Create, edit, duplicate, and delete connection profiles. +- Store display names, descriptions, notes, groups, tags, environment labels, and favourites. +- Search profiles by name, host, or tag. +- Filter profiles by protocol, group, tag, and favourite status. +- Test basic host and port reachability before connecting. +- Import existing OpenSSH config entries from `~/.ssh/config`, including hostnames, usernames, ports, identity files, and jump hosts. +- Export and import profiles using encrypted `.nexus` bundles. +- Choose import behaviour for duplicates: skip, overwrite, or duplicate. + +### Multi-session workspace + +- Tabbed sessions with connection state indicators. +- Pin tabs to keep important systems visible. +- Rename sessions. +- Reorder tabs. +- Close one session or all sessions. +- Split panes vertically or horizontally. +- Resize split panes. +- Switch tabs by keyboard shortcut. +- Broadcast input to multiple connected sessions. +- Show disconnected overlays with a one-click reconnect action. +- Auto-reconnect support for SSH, Telnet, and RAW TCP profiles. +- Command palette for fast access to connections, settings, terminal actions, security tools, import/export, snippets, triggers, and local terminal. + +### Terminal experience + +- xterm.js-powered terminal rendering. +- Configurable terminal font family, font size, scrollback, cursor style, and colour theme. +- Search within terminal output. +- Search options for case sensitivity, regular expressions, and whole-word matching. +- Copy, paste, select all, clear, and reset from the terminal context menu. +- Clipboard-aware paste behaviour. +- Clickable web links where supported. +- Alternate-screen scroll handling for tools such as `tmux`, `screen`, and `byobu`. +- Export terminal scrollback as plain text. +- Send break signals where supported. +- Ping active sessions to check responsiveness. + +### SSH capabilities + +- Password authentication. +- Public key authentication. +- Keyboard-interactive authentication. +- SSH agent authentication. +- SSH agent forwarding. +- Private key path discovery from `~/.ssh`. +- SSH key manager for listing, generating, copying, and deleting key pairs. +- Ed25519, RSA, and ECDSA key generation. +- Optional private key passphrase handling. +- TOTP / 2FA secret storage and code generation for keyboard-interactive login flows. +- Host key verification policies per profile: Strict, Ask, Accept new, and Insecure. +- Known hosts storage and management. +- Jump host support for bastion and multi-hop access. +- SOCKS5 proxy support. +- HTTP CONNECT proxy support. +- Local and remote port forwarding. +- Runtime port forward management for active SSH sessions. +- Startup commands. +- Custom environment variables. +- Terminal type configuration. +- Keepalive interval and missed-keepalive settings. +- Compression option. +- Expect-style login scripts with wait and send steps. +- Built-in SFTP browser for file transfer. + +### SFTP file browser + +- Open a side panel for SSH file browsing. +- Navigate remote folders with breadcrumbs. +- Go directly to a remote path. +- Bookmark remote locations. +- Refresh remote listings. +- Create folders. +- Upload files. +- Download single files or multiple selected files. +- Rename files and folders. +- Delete files and folders. +- Multi-select support for batch actions. +- Resizable SFTP panel alongside the terminal. +- Drag and drop files into an SSH terminal. +- Choose whether dropped files are uploaded or pasted into the terminal as text. +- Upload directory picker with folder filtering, breadcrumbs, keyboard navigation, and permission badges. +- Permission labels for read-only and inaccessible folders. + +### Telnet capabilities + +- Telnet option negotiation. +- Terminal type negotiation. +- NAWS window-size updates. +- Local echo option. +- Line mode option. +- Configurable line endings: LF, CR, CRLF, or none. +- Configurable encoding: UTF-8, ASCII, and ISO 8859-1. +- Negotiation debug mode. +- Expect-style login scripts. +- Auto-reconnect with configurable retry count. +- Break signal support. + +### RAW TCP capabilities + +- Direct TCP socket sessions. +- Text, hex, and split data views. +- Send data as text, hex, or escaped bytes. +- Quick insert controls for common bytes such as NUL, CR, LF, ETX, and EOT. +- Payload presets for reusable test messages. +- Optional timestamps. +- Optional send/receive direction labels. +- Pause and resume display updates. +- Clear the capture buffer. +- Save RAW session captures. +- Configurable encoding and line endings. +- Connect and read timeouts. +- Auto-reconnect option. + +### VNC capabilities + +- VNC remote desktop sessions. +- noVNC-based viewer inside the Nexus workspace. +- Password prompt and password storage modes. +- View-only mode. +- Scale-to-fit display. +- Shared-session option. +- Quality setting. +- SSH tunnel option for VNC traffic. +- SSH tunnel authentication by password, public key, or SSH agent. + +### GitHub capabilities + +- GitHub profile type using a personal access token. +- GitHub.com and GitHub Enterprise API base URL support. +- Repository viewer grouped by owner or organisation. +- Repository filtering by name, description, or language. +- Repositories sorted by latest commit activity. +- Shows repository metadata such as language, stars, private/fork/archived state, latest committer, and latest commit time. +- Manual refresh. +- Configurable refresh interval. +- Open repositories in the browser. +- Download repository ZIP archives. + +### Automation and reusable workflows + +- Snippet library for saved commands. +- Snippet folders, descriptions, and tags. +- Send snippets to active sessions. +- Snippet variables before sending commands. +- Broadcast input mode for running the same command across connected sessions. +- Trigger rules that watch terminal output. +- Trigger matching by text or regular expression. +- Trigger actions for notifications, tab highlighting, sound, or combined notify/highlight behaviour. +- Expect-style login scripts for SSH and Telnet. +- Startup commands for SSH sessions. + +### Session recording + +- Start and stop terminal recording per session. +- Recording indicator on active tabs. +- Recordings saved in asciicast v2-compatible format. +- Recording browser for listing saved recordings. +- Playback controls. +- Delete saved recordings. +- Recording metadata includes title, dimensions, duration, size, creation time, and file path. + +### Security and credential handling + +- OS keychain first where available, including macOS Keychain, Linux Secret Service, and Windows Credential Manager. +- Encrypted vault fallback. +- Vault encryption with AES-256-GCM and Argon2id-derived keys. +- Secret store lock and unlock workflow. +- Per-profile secret modes: Saved, Session only, and Ask every time. +- Encrypted storage for TOTP secrets. +- Known host verification and known host management. +- Audit log for security-relevant operations. +- Audit entries for connections, disconnections, profile changes, secret access, imports, and exports. +- Secrets are not written into audit entries. +- Encrypted profile exports with password-based AES-256-GCM encryption and Argon2id key derivation. +- Plain profile export can redact inline secrets. + +### Settings and maintenance + +- Application theme setting. +- Terminal theme selection. +- Default protocol preference. +- Default terminal font, size, scrollback, and cursor settings. +- Confirm-on-close and confirm-on-disconnect preferences. +- Status bar visibility setting. +- Sidebar width preference. +- Native paste preference. +- Local shell selection. +- Secret store settings. +- Known hosts management. +- Help and troubleshooting content inside settings. +- Background and manual update checks. +- Update checks report availability, current version, latest version, release notes, release URL, and publish date. + +## Website-ready summary + +### Suggested headline + +Nexus Terminal: one desktop workspace for SSH, files, sockets, remote desktops, and repositories. + +### Suggested subheadline + +Manage SSH, Telnet, RAW TCP, VNC, GitHub, and local shell sessions from a fast tabbed interface with secure credentials, SFTP file transfer, split panes, snippets, recordings, and automation built in. + +### Short product description + +Nexus Terminal helps developers, operators, support engineers, and homelab users manage many connections without juggling separate tools. Save profiles, organise them with groups and tags, connect in tabs or split panes, move files over SFTP, inspect raw TCP traffic, open VNC desktops, browse GitHub repositories, and keep credentials protected with keychain and encrypted-vault storage. + +### Product page feature bullets + +- All-in-one connection workspace for SSH, Telnet, RAW TCP, VNC, GitHub, and local shells. +- Fast multi-session interface with tabs, pinned sessions, split panes, reconnect, and broadcast input. +- Rich SSH support including agent auth, TOTP, jump hosts, proxies, port forwarding, startup commands, login scripts, and host key policies. +- Built-in SFTP browser with upload, download, rename, delete, folders, bookmarks, drag-and-drop upload, and permission-aware directory picking. +- RAW TCP tooling with text, hex, split views, payload presets, captures, timestamps, and byte-level sending. +- VNC viewer with scale-to-fit, view-only mode, quality controls, shared sessions, and optional SSH tunnelling. +- GitHub repository browser with organisation grouping, filtering, activity sorting, metadata, ZIP downloads, and GitHub Enterprise support. +- Secure credential model using OS keychain first, encrypted vault fallback, per-profile secret modes, known hosts, encrypted exports, and audit logging. +- Productivity tools including command palette, snippets, trigger rules, session recording, terminal search, scrollback export, and local terminal. ## Who it is for -Nexus Terminal suits operators, developers, and support teams who need to connect to many endpoints quickly, keep sessions organised, and avoid re-entering credentials. +- Developers who connect to many application servers, staging environments, and production hosts. +- Platform and DevOps teams who need organised, secure access across infrastructure. +- Support teams who troubleshoot customer systems, network appliances, and legacy Telnet devices. +- Network and hardware engineers who need RAW TCP and Telnet tools beside normal SSH workflows. +- Homelab users who want SSH, VNC, file transfer, and repo access in one desktop app. ## Learn more diff --git a/frontend/package.json b/frontend/package.json index 9dafde6..4ab00db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "bundle:novnc": "node scripts/bundle-novnc.js", "build": "node scripts/build.js", "dev": "node scripts/build.js --watch", + "test": "node scripts/test-regressions.js", "postinstall": "npm run install:assets && npm run bundle:novnc" }, "dependencies": { diff --git a/frontend/scripts/test-regressions.js b/frontend/scripts/test-regressions.js new file mode 100644 index 0000000..86f9668 --- /dev/null +++ b/frontend/scripts/test-regressions.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node +const fs = require("fs"); +const path = require("path"); + +const root = path.resolve(__dirname, ".."); +const repo = path.resolve(root, ".."); + +function read(rel) { + return fs.readFileSync(path.join(repo, rel), "utf8"); +} + +function assertContains(file, needle, message) { + const text = read(file); + if (!text.includes(needle)) { + throw new Error(`${message}\nMissing ${JSON.stringify(needle)} in ${file}`); + } +} + +function assertNotContains(file, needle, message) { + const text = read(file); + if (text.includes(needle)) { + throw new Error(`${message}\nUnexpected ${JSON.stringify(needle)} in ${file}`); + } +} + +assertContains( + "frontend/src/js/terminal.js", + "escapeHtml(title)", + "file-drop prompt titles must escape filenames containing HTML characters", +); +assertContains( + "frontend/src/js/terminal.js", + "SFTPUploadStart", + "drag-drop upload must use chunked SFTP streaming", +); +assertContains( + "frontend/src/js/terminal.js", + "SFTPUploadChunk", + "drag-drop upload must send bounded chunks", +); +assertNotContains( + "frontend/src/js/terminal.js", + "Use SFTP Upload for files over 25 MB", + "drag-drop upload must not reject files only because they exceed the old inline limit", +); +assertContains( + "frontend/src/js/app.js", + "confirmDirectVNCProfile", + "direct VNC connections must show an explicit warning", +); +assertContains( + "frontend/src/js/profiles.js", + "skipDirectVNCWarning", + "save-and-connect should not show duplicate direct VNC prompts", +); +assertContains( + "frontend/src/js/recordings.js", + "import-status", + "profile import failures must be visible inside the import modal", +); +assertContains( + "frontend/src/js/recordings.js", + "_formatImportError", + "profile import failures must be formatted without using HTML injection", +); +assertContains( + "internal/api/api_test.go", + "TestCreateProfileRejectsUnsafeGitHubBaseURL", + "GitHub HTTP/HTTPS URL validation must stay covered", +); + +console.log("frontend regression checks passed"); diff --git a/frontend/src/css/components.css b/frontend/src/css/components.css index 592e528..64d6835 100644 --- a/frontend/src/css/components.css +++ b/frontend/src/css/components.css @@ -116,6 +116,19 @@ border-color: #e53935; } +/* Warning */ +.btn-warning { + background: var(--warning); + color: #1a1a1e; + border-color: var(--warning); +} +.btn-warning:hover { + filter: brightness(1.05); +} +.btn-warning:active { + filter: brightness(0.95); +} + /* Size variants */ .btn-sm { height: 26px; diff --git a/frontend/src/css/recordings.css b/frontend/src/css/recordings.css index 67d52b9..0bff67c 100644 --- a/frontend/src/css/recordings.css +++ b/frontend/src/css/recordings.css @@ -18,6 +18,34 @@ } /* Recordings list in browser modal */ +.recording-storage { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0 0 12px; + margin-bottom: 12px; + border-bottom: 1px solid var(--border, #3f3f46); +} + +.recording-storage-summary { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 12px; + color: var(--text-secondary, #a1a1aa); +} + +.recording-cleanup-controls { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; + gap: 8px; + align-items: center; +} + +.recording-cleanup-status { + min-height: 16px; +} + .recordings-list { display: flex; flex-direction: column; @@ -66,6 +94,12 @@ margin-left: 12px; } +@media (max-width: 640px) { + .recording-cleanup-controls { + grid-template-columns: 1fr; + } +} + /* Playback modal — larger to fit a terminal */ .modal-playback { width: 90vw; diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 3543136..c1aabd0 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -744,7 +744,7 @@ * Opens a new session for the given profile ID. * @param {string} profileId */ - async connectProfile(profileId) { + async connectProfile(profileId, options = {}) { const profile = this.state.getProfile(profileId); if (!profile) { this.notifications.error("Profile not found"); @@ -774,6 +774,14 @@ if (decision !== "new") return; } + if ( + !options.skipDirectVNCWarning && + this._requiresDirectVNCWarning(profile) + ) { + const ok = await this.confirmDirectVNCProfile(profile, "connect"); + if (!ok) return; + } + try { const sessionInfo = await window.go.api.Service.ConnectSession(profileId); @@ -977,7 +985,9 @@ */ _confirmReuseSession(profile, session) { const createElement = window.NexusTerminal.createElement; - const label = (profile.displayName || profile.host || "Untitled").trim(); + const label = window.NexusTerminal.escapeHtml( + (profile.displayName || profile.host || "Untitled").trim(), + ); const stateLabel = session.state === "connected" ? "connected" : "connecting"; @@ -1033,6 +1043,74 @@ }); } + /** + * Reports whether a profile connects directly to VNC without SSH tunnelling. + * @param {Object} profile + * @returns {boolean} + * @private + */ + _requiresDirectVNCWarning(profile) { + return !!( + profile && + profile.protocol === "vnc" && + profile.vnc && + !profile.vnc.sshTunnel + ); + } + + /** + * Warns before saving or connecting direct VNC profiles. + * @param {Object} profile + * @param {"save"|"connect"} action + * @returns {Promise} + */ + confirmDirectVNCProfile(profile, action) { + const { createElement, createModalShell } = window.NexusTerminal; + const label = ( + profile.displayName || + profile.vnc?.vncHost || + profile.host || + "VNC" + ).trim(); + const actionLabel = action === "connect" ? "Connect" : "Save"; + + return new Promise((resolve) => { + const shell = createModalShell("Direct VNC Connection", "modal-sm"); + const bodyText = createElement("p", "", { + textContent: + label + + " is configured without an SSH tunnel. VNC traffic may be visible to the network unless the server is otherwise protected.", + }); + bodyText.style.fontSize = "var(--text-sm)"; + bodyText.style.color = "var(--text-secondary)"; + shell.body.appendChild(bodyText); + + const cancelBtn = createElement("button", "btn btn-sm", { + textContent: "Cancel", + }); + const okBtn = createElement( + "button", + "btn btn-sm btn-warning direct-vnc-confirm", + { textContent: actionLabel }, + ); + shell.footer.appendChild(cancelBtn); + shell.footer.appendChild(okBtn); + + const close = (value) => { + shell.remove(); + resolve(value); + }; + + shell.closeButton.addEventListener("click", () => close(false)); + cancelBtn.addEventListener("click", () => close(false)); + okBtn.addEventListener("click", () => close(true)); + shell.backdrop.addEventListener("click", (e) => { + if (e.target === shell.backdrop) close(false); + }); + shell.show(); + }); + } + // ── Import / Export ────────────────────────────────────────────── /** diff --git a/frontend/src/js/profiles.js b/frontend/src/js/profiles.js index 15a3357..d30e512 100644 --- a/frontend/src/js/profiles.js +++ b/frontend/src/js/profiles.js @@ -2314,6 +2314,17 @@ return; } + if ( + this.app._requiresDirectVNCWarning && + this.app._requiresDirectVNCWarning(this._profile) + ) { + const ok = await this.app.confirmDirectVNCProfile( + this._profile, + andConnect ? "connect" : "save", + ); + if (!ok) return; + } + try { let saved; if (this._isNew) { @@ -2329,7 +2340,7 @@ this.close(); if (andConnect && saved && saved.id) { - this.app.connectProfile(saved.id); + this.app.connectProfile(saved.id, { skipDirectVNCWarning: true }); } } catch (err) { this.app.notifications.error("Save failed: " + err); diff --git a/frontend/src/js/recordings.js b/frontend/src/js/recordings.js index 3e2513a..a87720c 100644 --- a/frontend/src/js/recordings.js +++ b/frontend/src/js/recordings.js @@ -70,8 +70,11 @@ class RecordingManager { */ async openBrowser() { try { - const recordings = await window.go.api.Service.ListRecordings(); - this._showBrowserModal(recordings || []); + const [recordings, storageInfo] = await Promise.all([ + window.go.api.Service.ListRecordings(), + window.go.api.Service.GetRecordingStorageInfo(), + ]); + this._showBrowserModal(recordings || [], storageInfo || {}); } catch (err) { this.app.notifications.error('Failed to list recordings: ' + err); } @@ -133,9 +136,10 @@ class RecordingManager { /** * Renders a modal listing all saved recordings. * @param {Array} recordings + * @param {Object} storageInfo * @private */ - _showBrowserModal(recordings) { + _showBrowserModal(recordings, storageInfo = {}) { const backdrop = createElement('div', 'modal-backdrop'); const modal = createElement('div', 'modal modal-lg'); @@ -164,12 +168,36 @@ class RecordingManager { listHTML += ''; } + const totalSize = _formatBytes(storageInfo.totalBytes || 0); + const count = storageInfo.count ?? recordings.length; + modal.innerHTML = ` +