diff --git a/.bash_logout b/.bash_logout
deleted file mode 100644
index de4f5f75..00000000
--- a/.bash_logout
+++ /dev/null
@@ -1,7 +0,0 @@
-# ~/.bash_logout: executed by bash(1) when login shell exits.
-
-# when leaving the console clear the screen to increase privacy
-
-if [ "$SHLVL" = 1 ]; then
- [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q
-fi
diff --git a/.docker/buildx/.lock b/.docker/buildx/.lock
deleted file mode 100644
index e69de29b..00000000
diff --git a/.docker/buildx/current b/.docker/buildx/current
deleted file mode 100644
index f34f695d..00000000
--- a/.docker/buildx/current
+++ /dev/null
@@ -1 +0,0 @@
-{"Key":"unix:///var/run/docker.sock","Name":"","Global":false}
\ No newline at end of file
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 7a44e9c1..05961696 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -1,4 +1,5 @@
name: Build and Push Docker Image
+
on:
push:
branches:
@@ -8,7 +9,7 @@ on:
tag_name:
description: "Custom tag name for the Docker image"
required: false
- default: "development-latest"
+ default: ""
jobs:
build:
@@ -16,49 +17,64 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
-
+
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
-
+
- name: Install Dependencies and Build Frontend
run: |
cd frontend
- npm install
+ npm ci
npm run build
-
+
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v1
-
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
+
- name: Login to Docker Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
+
- name: Determine Docker image tag
run: |
echo "REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- if [ -n "${{ github.event.inputs.tag_name }}" ]; then
- IMAGE_TAG="${{ github.event.inputs.tag_name }}"
+ if [ "${{ github.event.inputs.tag_name }}" == "" ]; then
+ IMAGE_TAG="${{ github.ref_name }}-development-latest"
else
- IMAGE_TAG="development-latest"
+ IMAGE_TAG="${{ github.event.inputs.tag_name }}"
fi
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
-
+
- name: Build and Push Docker Image
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile
push: true
- tags: ghcr.io/${{ env.REPO_OWNER }}/ssh-project:${{ env.IMAGE_TAG }}
+ tags: ghcr.io/${{ env.REPO_OWNER }}/termix:${{ env.IMAGE_TAG }}
labels: org.opencontainers.image.source=https://github.com/${{ github.repository }}
-
- - name: Cleanup Docker Images
- run: docker image prune -af
\ No newline at end of file
+
+ - name: Notify via ntfy
+ run: |
+ curl -d "Docker image build and push completed successfully for tag: ${{ env.IMAGE_TAG }}" \
+ https://ntfy.karmaashomepage.online/termix-build
+
+ - name: Delete all untagged image versions
+ uses: quartx-analytics/ghcr-cleaner@v1
+ with:
+ owner-type: user
+ token: ${{ secrets.GHCR_TOKEN }}
+ repository-owner: ${{ github.repository_owner }}
+ delete-untagged: true
+
+ - name: Cleanup Docker Images Locally
+ run: |
+ docker image prune -af
+ docker system prune -af --volumes
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 7b702ae0..563fa9a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -133,6 +133,8 @@ yarn-error.log*
.sudo_as_admin_successful
.wget-hsts
.git-credentials
+.docker/
+.bash_logout
# VSCode Files
.vscode-server/
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 00000000..f85d9ef2
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Termix
\ No newline at end of file
diff --git a/.idea/Termix.iml b/.idea/Termix.iml
new file mode 100644
index 00000000..24643cc3
--- /dev/null
+++ b/.idea/Termix.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 00000000..43d2b1c8
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index f7db5cae..a12919a6 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1ddf..94a25f7f 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 00000000..dde1be2b
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,467 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "lastFilter": {}
+}
+ {
+ "selectedUrlAndAccountId": {
+ "url": "https://github.com/LukeGus/Termix",
+ "accountId": "f107d505-1915-4c73-a3f4-e0440737e1dc"
+ }
+}
+ {
+ "customColor": "636378de",
+ "associatedIndex": 8
+}
+
+
+
+
+
+
+
+
+ {
+ "keyToString": {
+ "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "Shell Script.Node Server.js Start.executor": "Run",
+ "Shell Script.Run backend and frontend.executor": "Run",
+ "Shell Script.run_backend_frontend.executor": "Run",
+ "git-widget-placeholder": "release-1.0",
+ "ignore.virus.scanning.warn.message": "true",
+ "last_opened_file_path": "C:/Users/Luke/Documents/Personal Projects/Termix/frontend",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "npm.run_start.executor": "Run",
+ "npm.run_start_frontend.executor": "Run",
+ "npm.run_start_node_backend.executor": "Run",
+ "npm.run_start_vite.executor": "Run",
+ "npm.start.executor": "Run",
+ "settings.editor.selected.configurable": "ml.llm.LLMConfigurable",
+ "ts.external.directory.path": "D:\\Program Files (x86)\\Applications\\Jetbrains Webstorm\\WebStorm 2024.3.1\\plugins\\javascript-plugin\\jsLanguageServicesImpl\\external",
+ "vue.rearranger.settings.migration": "true"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1733439468142
+
+
+ 1733439468142
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1733447944692
+
+
+
+ 1733447944692
+
+
+
+ 1733448864234
+
+
+
+ 1733448864234
+
+
+
+ 1733449234908
+
+
+
+ 1733449234908
+
+
+
+ 1733449331913
+
+
+
+ 1733449331913
+
+
+
+ 1733449760767
+
+
+
+ 1733449760767
+
+
+
+ 1733450304640
+
+
+
+ 1733450304640
+
+
+
+ 1733450681262
+
+
+
+ 1733450681262
+
+
+
+ 1733451925922
+
+
+
+ 1733451925922
+
+
+
+ 1733532896448
+
+
+
+ 1733532896448
+
+
+
+ 1733538097802
+
+
+
+ 1733538097802
+
+
+
+ 1733539381433
+
+
+
+ 1733539381433
+
+
+
+ 1733553128900
+
+
+
+ 1733553128900
+
+
+
+ 1733553902375
+
+
+
+ 1733553902375
+
+
+
+ 1733555933987
+
+
+
+ 1733555933987
+
+
+
+ 1733556269270
+
+
+
+ 1733556269270
+
+
+
+ 1733556428424
+
+
+
+ 1733556428424
+
+
+
+ 1733556699034
+
+
+
+ 1733556699034
+
+
+
+ 1733556917800
+
+
+
+ 1733556917800
+
+
+
+ 1733557286584
+
+
+
+ 1733557286584
+
+
+
+ 1733557337662
+
+
+
+ 1733557337662
+
+
+
+ 1733557558085
+
+
+
+ 1733557558085
+
+
+
+ 1733557773727
+
+
+
+ 1733557773727
+
+
+
+ 1733558138278
+
+
+
+ 1733558138278
+
+
+
+ 1733558347319
+
+
+
+ 1733558347319
+
+
+
+ 1733558781194
+
+
+
+ 1733558781194
+
+
+
+ 1733558952696
+
+
+
+ 1733558952696
+
+
+
+ 1733603759658
+
+
+
+ 1733603759658
+
+
+
+ 1733688870582
+
+
+
+ 1733688870582
+
+
+
+ 1733691729582
+
+
+
+ 1733691729582
+
+
+
+ 1733691982293
+
+
+
+ 1733691982293
+
+
+
+ 1733692181011
+
+
+
+ 1733692181011
+
+
+
+ 1733692962039
+
+
+
+ 1733692962039
+
+
+
+ 1733886236040
+
+
+
+ 1733886236040
+
+
+
+ 1733887722496
+
+
+
+ 1733887722496
+
+
+
+ 1733887980750
+
+
+
+ 1733887980750
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..21146d9c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Luke Gustafson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 895bf2a4..678c711b 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,49 @@
-SSH is silly
\ No newline at end of file
+# Repo Stats
+
+
+
+#### Top Technologies
+[](#)
+[](#)
+[](#)
+[](#)
+[](#)
+[](#)
+
+
+
+
+
+
+
+# Description
+Termix is an open-source forever free self-hosted SSH (other protocols planned, see [Planned Features](#planned-features)) management panel inspired by [Nexterm](https://github.com/gnmyt/Nexterm). Its purpose is to provide an all-in-one docker-hosted web solution to manage your servers in one easy place. I'm using this project to help me learn [React](https://github.com/facebook/react), [Vite](https://github.com/vitejs/vite-plugin-react), and [Docker](https://www.docker.com) but also because I could never settle on a server management software that I enjoyed to use.
+
+> [!WARNING]
+> This app is in the VERY early stages of development. Expect bugs, data loss, and possibly even security issues!
+
+# Planned Features
+- [x] SSH
+- [ ] VNC
+- [ ] RDP
+- [ ] SMTP (build in file transfer)
+- [ ] Split Screen & Tabs
+- [ ] ChatGPT/Ollama Integration (for commands)
+- [ ] Login Screen
+
+# How Termix is Different
+Before developing Termix, I faced the issue of a server management panel that did not have every feature I liked. [Guacamole](https://guacamole.apache.org/) had poor copy/paste abilities and a poor UI. [Shellngn](https://shellngn.com/) was too expensive and all other alternatives had one major problem with them. I plan to develop the management panel of my dreams with even an AI integration for those pesky commands I always forget the syntax of.
+
+# Installation
+View the Termix [Wiki](https://github.com/LukeGus/Termix/wiki) for information on how to install Termix. You can also use these links to go directly to guide. [Docker](https://github.com/LukeGus/Termix/wiki/Docker) or [Manual](https://github.com/LukeGus/Termix/wiki/Manual).
+
+# Known Bugs
+### Please create an [Issue](https://github.com/LukeGus/Termix/issues) if you find any problems!
+Start session button stays connected even if SSH fails to connect.
+
+# Show-off
+
+
+
+# License
+Distributed under the MIT license. See LICENSE for more information.
diff --git a/backend/package-lock.json b/backend/package-lock.json
index d77add2f..16cf5968 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -11,6 +11,8 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
+ "@xterm/addon-fit": "^0.10.0",
+ "@xterm/xterm": "^5.5.0",
"express": "^4.21.1",
"guacamole-common-js": "^1.5.0",
"http-proxy-middleware": "^3.0.3",
@@ -1501,6 +1503,19 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
+ "node_modules/@xterm/addon-fit": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
+ "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
+ "peerDependencies": {
+ "@xterm/xterm": "^5.0.0"
+ }
+ },
+ "node_modules/@xterm/xterm": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
+ },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -2288,10 +2303,9 @@
}
},
"node_modules/express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
- "license": "MIT",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -2312,7 +2326,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -2327,6 +2341,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/debug": {
@@ -3394,9 +3412,9 @@
"optional": true
},
"node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@@ -3404,6 +3422,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -3539,10 +3558,9 @@
}
},
"node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
- "license": "MIT"
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"node_modules/pathe": {
"version": "1.1.1",
@@ -5774,6 +5792,17 @@
}
}
},
+ "@xterm/addon-fit": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
+ "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
+ "requires": {}
+ },
+ "@xterm/xterm": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
+ },
"abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -6320,9 +6349,9 @@
}
},
"express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -6343,7 +6372,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -7096,9 +7125,9 @@
"optional": true
},
"nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true
},
"negotiator": {
@@ -7185,9 +7214,9 @@
"dev": true
},
"path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"pathe": {
"version": "1.1.1",
diff --git a/backend/package.json b/backend/package.json
index 576bf6c3..19a0692a 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -6,6 +6,8 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
+ "@xterm/addon-fit": "^0.10.0",
+ "@xterm/xterm": "^5.5.0",
"express": "^4.21.1",
"guacamole-common-js": "^1.5.0",
"http-proxy-middleware": "^3.0.3",
@@ -55,4 +57,4 @@
"vite": "^4.5.5",
"vitest": "^0.34.6"
}
-}
\ No newline at end of file
+}
diff --git a/backend/server.js b/backend/server.js
index 56bf24c8..e041c2b2 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -2,97 +2,104 @@ const WebSocket = require('ws');
const ssh2 = require('ssh2');
const http = require('http');
-// Create an HTTP server to serve WebSocket connections
const server = http.createServer((req, res) => {
- res.writeHead(200, { 'Content-Type': 'text/plain' });
- res.end('WebSocket server is running\n');
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
+ res.end('WebSocket server is running\n');
});
-// Create a WebSocket server attached to the HTTP server
const wss = new WebSocket.Server({ server });
-// WebSocket connection handling
wss.on('connection', (ws) => {
- console.log('WebSocket connection established');
+ console.log('WebSocket connection established');
- let conn = null; // Declare SSH client outside to manage lifecycle
+ let conn = new ssh2.Client();
+ let stream = null;
+ let currentCols = 80;
+ let currentRows = 24;
+ let interval = null;
- ws.on('message', (message) => {
- try {
- const data = JSON.parse(message); // Try parsing the incoming message as JSON
-
- // Check if message contains SSH connection details
- if (data.host && data.username && data.password) {
- if (conn) {
- conn.end(); // Close any previous connection before starting a new one
+ const resizeTerminal = (cols, rows) => {
+ if (stream && stream.setWindow) {
+ stream.setWindow(rows, cols, rows * 100, cols * 100); // Adjust terminal size
+ console.log(`Terminal resized successfully: cols=${cols}, rows=${rows}`);
}
+ };
- conn = new ssh2.Client(); // Create a new SSH connection instance
+ ws.on('message', (message) => {
+ const messageStr = message.toString();
- // When the SSH connection is ready
- conn.on('ready', () => {
- console.log('SSH Connection established');
-
- // Start an interactive shell session
- conn.shell((err, stream) => {
- if (err) {
- console.log(`SSH Error: ${err}`);
- ws.send(`Error: ${err}`);
- return;
+ let data;
+ try {
+ if (messageStr.trim().startsWith('{')) {
+ data = JSON.parse(messageStr);
+ } else if (stream && stream.writable) {
+ stream.write(messageStr);
+ return;
}
-
- // Handle data from SSH session
- stream.on('data', (data) => {
- console.log(`SSH Output: ${data}`);
- ws.send(data.toString()); // Send the SSH output back to WebSocket client
- });
-
- // Handle stream close event
- stream.on('close', () => {
- console.log('SSH stream closed');
- conn.end();
- });
-
- // When the WebSocket client sends a message (from terminal input), forward it to the SSH stream
- ws.on('message', (message) => {
- console.log(`Received message from WebSocket: ${message}`);
- stream.write(message); // Write the message (input) to the SSH shell
- });
- });
- }).on('error', (err) => {
- console.log('SSH Connection Error: ', err);
- ws.send(`SSH Error: ${err}`);
- }).connect({
- host: data.host, // Host provided from the client
- port: 22, // Default SSH port
- username: data.username, // Username provided from the client
- password: data.password, // Password provided from the client
- });
- }
- } catch (error) {
- // If message is not valid JSON (i.e., terminal input), treat it as raw text and send it to SSH
- console.log('Received non-JSON message, sending to SSH session:', message);
- if (conn) {
- const stream = conn._stream; // Access the SSH stream directly
- if (stream && stream.writable) {
- stream.write(message); // Write raw input message to SSH stream
+ } catch (error) {
+ console.error('Failed to process message:', error);
+ return;
}
- } else {
- console.error('SSH connection is not established yet.');
- }
- }
- });
- // Handle WebSocket close event
- ws.on('close', () => {
- console.log('WebSocket closed');
- if (conn) {
- conn.end(); // Close SSH connection when WebSocket client disconnects
- }
- });
+ if (data?.host && data.port && data.username && data.password) {
+ conn.on('ready', () => {
+ console.log('SSH Connection established');
+
+ interval = setInterval(() => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.ping();
+ } else {
+ clearInterval(interval);
+ }
+ }, 15000);
+
+ conn.shell({ term: 'xterm', cols: currentCols, rows: currentRows }, (error, newStream) => {
+ if (error) {
+ console.error(`SSH Shell Error: ${error}`);
+ ws.send(`Error: Could not establish a shell: ${error.message}`);
+ return;
+ }
+
+ stream = newStream;
+
+ stream.on('data', (chunk) => {
+ ws.send(chunk.toString());
+ });
+
+ stream.on('close', () => {
+ console.log('SSH stream closed');
+ conn.end();
+ });
+ });
+ }).on('error', (err) => {
+ console.log('SSH Connection Error:', err);
+ ws.send(`SSH Connection Error: ${err.message}`);
+ }).connect({
+ host: data.host,
+ port: data.port,
+ username: data.username,
+ password: data.password,
+ keepaliveInterval: 10000,
+ keepaliveCountMax: 5,
+ });
+ } else if (data?.cols && data.rows) {
+ currentCols = data.cols;
+ currentRows = data.rows;
+ resizeTerminal(currentCols, currentRows);
+ }
+ });
+
+ ws.on('close', () => {
+ console.log('WebSocket closed');
+ if (interval) {
+ clearInterval(interval);
+ }
+ if (conn) {
+ conn.end();
+ }
+ });
});
-// Start the WebSocket server on port 8081
server.listen(8081, () => {
- console.log('WebSocket server is listening on ws://localhost:8081');
+ console.log('WebSocket server is listening on ws://localhost:8081');
});
\ No newline at end of file
diff --git a/frontend/favicon.ico b/frontend/favicon.ico
new file mode 100644
index 00000000..9dfe8173
Binary files /dev/null and b/frontend/favicon.ico differ
diff --git a/frontend/index.html b/frontend/index.html
index 6eb6d291..945c1192 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,12 +2,12 @@
-
+
- React App
+ Termix
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 795ad30d..5ca56465 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -11,6 +11,8 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
+ "@xterm/addon-fit": "^0.10.0",
+ "@xterm/xterm": "^5.5.0",
"express": "^4.21.1",
"guacamole-common-js": "^1.5.0",
"http-proxy-middleware": "^3.0.3",
@@ -1501,6 +1503,19 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
+ "node_modules/@xterm/addon-fit": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
+ "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
+ "peerDependencies": {
+ "@xterm/xterm": "^5.0.0"
+ }
+ },
+ "node_modules/@xterm/xterm": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
+ },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -2288,10 +2303,9 @@
}
},
"node_modules/express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
- "license": "MIT",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -2312,7 +2326,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -2327,6 +2341,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/debug": {
@@ -3394,9 +3412,9 @@
"optional": true
},
"node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@@ -3404,6 +3422,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -3539,10 +3558,9 @@
}
},
"node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
- "license": "MIT"
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"node_modules/pathe": {
"version": "1.1.1",
@@ -5773,6 +5791,17 @@
}
}
},
+ "@xterm/addon-fit": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
+ "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
+ "requires": {}
+ },
+ "@xterm/xterm": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
+ },
"abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -6319,9 +6348,9 @@
}
},
"express": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -6342,7 +6371,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -7095,9 +7124,9 @@
"optional": true
},
"nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true
},
"negotiator": {
@@ -7184,9 +7213,9 @@
"dev": true
},
"path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"pathe": {
"version": "1.1.1",
diff --git a/frontend/package.json b/frontend/package.json
index 45136381..d8441a0b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,6 +6,8 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
+ "@xterm/addon-fit": "^0.10.0",
+ "@xterm/xterm": "^5.5.0",
"express": "^4.21.1",
"guacamole-common-js": "^1.5.0",
"http-proxy-middleware": "^3.0.3",
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 99b631b2..b51f4ea3 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -22,7 +22,6 @@
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
- border-radius: 5px;
position: relative;
}
@@ -63,7 +62,7 @@
.hide-sidebar-button {
position: fixed;
bottom: 10px;
- right: 25px;
+ right: 23px;
background-color: rgb(108, 108, 108);
color: white;
border: none;
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 69dada08..c7d21ece 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
-import { Terminal } from 'xterm';
+import { Terminal } from '@xterm/xterm';
import 'xterm/css/xterm.css';
-import { FitAddon } from 'xterm-addon-fit';
+import { FitAddon } from '@xterm/addon-fit';
import './App.css';
const App = () => {
@@ -10,98 +10,109 @@ const App = () => {
const fitAddon = useRef(null);
const socket = useRef(null);
const [host, setHost] = useState('');
+ const [port, setPort] = useState('22');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isSideBarHidden, setIsSideBarHidden] = useState(false);
useEffect(() => {
- console.log('Initializing terminal...');
terminal.current = new Terminal({
cursorBlink: true,
- theme: { background: '#1e1e1e', foreground: '#ffffff' },
+ theme: {
+ background: '#1e1e1e',
+ foreground: '#ffffff',
+ },
macOptionIsMeta: true,
allowProposedApi: true,
- fontSize: 14,
+ scrollback: 5000,
});
fitAddon.current = new FitAddon();
terminal.current.loadAddon(fitAddon.current);
- if (terminalRef.current) {
- terminal.current.open(terminalRef.current);
- console.log('Terminal opened successfully.');
- } else {
- console.error('Terminal reference is not valid!');
- }
+ terminal.current.open(terminalRef.current);
+ fitAddon.current.fit();
+ // Fit the terminal and send the size when needed
+ const fitAndNotifyResize = () => {
+ fitAddon.current.fit();
+ if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+ socket.current.send(JSON.stringify({
+ cols: terminal.current.cols,
+ rows: terminal.current.rows,
+ }));
+ }
+ };
+ window.addEventListener('resize', fitAndNotifyResize);
+
+ terminal.current.onResize(({ cols, rows }) => {
+ console.log(`Terminal resized to cols:${cols}, rows:${rows}`);
+ fitAndNotifyResize();
+ });
+
+ const handleConnectionEstablished = () => {
+ fitAndNotifyResize();
+ };
+
+ window.addEventListener('connection-established', handleConnectionEstablished);
+
+ // Monitor terminal data (activity)
terminal.current.onData((data) => {
if (socket.current && socket.current.readyState === WebSocket.OPEN) {
socket.current.send(data);
}
});
- const resizeTerminal = () => {
- if (terminalRef.current) {
- fitAddon.current.fit();
- notifyServerOfResize();
- }
- };
-
- const notifyServerOfResize = () => {
- if (socket.current && socket.current.readyState === WebSocket.OPEN) {
- const { rows, cols } = terminal.current;
- socket.current.send(
- JSON.stringify({
- type: 'resize',
- rows,
- cols,
- height: terminalRef.current.offsetHeight,
- width: terminalRef.current.offsetWidth,
- })
- );
- }
- };
-
- resizeTerminal();
- window.addEventListener('resize', resizeTerminal);
-
return () => {
terminal.current.dispose();
if (socket.current) {
socket.current.close();
}
- window.removeEventListener('resize', resizeTerminal);
+ window.removeEventListener('resize', fitAndNotifyResize);
};
}, []);
const handleConnect = () => {
- console.log('Connecting...');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const wsUrl = `${protocol}//${window.location.host}/ws/`;
- console.log(`WebSocket URL: ${wsUrl}`);
+ const wsUrl = `${protocol}//${window.location.host}/ws/`; // Use current host and "/ws/" endpoint
+
+ if (!host || !username || !password) {
+ terminal.current.writeln('Please fill in all fields.');
+ return;
+ }
socket.current = new WebSocket(wsUrl);
socket.current.onopen = () => {
- console.log('WebSocket connection opened');
terminal.current.writeln(`Connected to WebSocket server at ${wsUrl}`);
- socket.current.send(JSON.stringify({ host, username, password }));
+ socket.current.send(
+ JSON.stringify({
+ host,
+ port,
+ username,
+ password,
+ rows: terminal.current.rows,
+ cols: terminal.current.cols
+ })
+ );
+
+ // Dispatch a custom event when connection is open
+ const event = new Event('connection-established');
+ window.dispatchEvent(event);
+
setIsConnected(true);
};
socket.current.onmessage = (event) => {
- console.log('Received message:', event.data);
terminal.current.write(event.data);
};
socket.current.onerror = (error) => {
- console.error('WebSocket error:', error);
terminal.current.writeln(`WebSocket error: ${error.message}`);
};
socket.current.onclose = () => {
- console.log('WebSocket connection closed');
terminal.current.writeln('Disconnected from WebSocket server.');
setIsConnected(false);
};
@@ -112,13 +123,33 @@ const App = () => {
};
const handleSideBarHiding = () => {
- setIsSideBarHidden((prevState) => !prevState);
- if (!isSideBarHidden) {
- setTimeout(() => {
- fitAddon.current.fit();
- notifyServerOfResize();
- }, 100);
- }
+ setIsSideBarHidden((prevState) => {
+ const newState = !prevState;
+ if (newState) {
+ setTimeout(() => {
+ // Add a delay to ensure layout settles before resize action
+ fitAddon.current.fit();
+ if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+ socket.current.send(JSON.stringify({
+ cols: terminal.current.cols,
+ rows: terminal.current.rows,
+ }));
+ }
+ }, 100); // Delay of 100 milliseconds
+ } else {
+ setTimeout(() => {
+ // Refit terminal when showing sidebar as well
+ fitAddon.current.fit();
+ if (socket.current && socket.current.readyState === WebSocket.OPEN) {
+ socket.current.send(JSON.stringify({
+ cols: terminal.current.cols,
+ rows: terminal.current.rows,
+ }));
+ }
+ }, 100); // Delay of 100 milliseconds
+ }
+ return newState;
+ });
};
return (
@@ -132,6 +163,12 @@ const App = () => {
value={host}
onChange={(e) => handleInputChange(e, setHost)}
/>
+ handleInputChange(e, setPort)}
+ />
{
-