diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index de2f6fcd..78599938 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -8,7 +8,7 @@ on: tag_name: description: "Custom tag name for the Docker image" required: false - default: "development-latest" + default: "" jobs: build: @@ -16,41 +16,43 @@ 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 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 + # If the tag_name input is an empty string, default to branch-name-development-latest + IMAGE_TAG="${{ github.ref_name }}-development-latest" else - IMAGE_TAG="development-latest" + # If tag_name is provided, use it + 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: @@ -64,6 +66,6 @@ jobs: run: | curl -d "Docker image build and push completed successfully for tag: ${{ env.IMAGE_TAG }}" \ https://ntfy.karmaashomepage.online/ssh-project-build - + - name: Cleanup Docker Images run: docker image prune -af \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index dd96d393..08fc9245 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,10 +4,12 @@ - + + + @@ -42,32 +44,32 @@ - { - "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": "alpha-1.0", - "ignore.virus.scanning.warn.message": "true", - "last_opened_file_path": "D:/Programming Projects/SSH-Project-JB", - "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" + +}]]> @@ -116,7 +118,7 @@ 1733439468142 - + @@ -214,7 +216,15 @@ 1733553128900 - + + + 1733553902375 + + + + 1733553902375 + + @@ -252,6 +262,7 @@ - + + \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index e3f89b27..02989734 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,21 +15,22 @@ const wss = new WebSocket.Server({ server }); wss.on('connection', (ws) => { console.log('WebSocket connection established'); - let conn = null; // Declare SSH client outside to manage lifecycle + let conn = null; + let stream = null; + let interval = null; ws.on('message', (message) => { try { - const data = JSON.parse(message); // Try parsing the incoming message as JSON + const data = JSON.parse(message); - // Check if message contains SSH connection details if (data.host && data.port && data.username && data.password) { if (conn) { - conn.end(); // Close any previous connection before starting a new one + conn.end(); } - conn = new ssh2.Client(); // Create a new SSH connection instance + conn = new ssh2.Client(); - const interval = setInterval(() => { + interval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.ping(); } else { @@ -37,78 +38,81 @@ wss.on('connection', (ws) => { } }, 15000); - // When the SSH connection is ready conn.on('ready', () => { console.log('SSH Connection established'); - - // Start an interactive shell session - conn.shell((err, stream) => { + conn.shell((err, sshStream) => { if (err) { console.log(`SSH Error: ${err}`); ws.send(`Error: ${err}`); 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 + stream = sshStream; + + // Send stty commands for resizing rows and columns + const resizeCommand = (rows, cols) => { + return `stty rows ${rows} cols ${cols}\n`; + }; + + // Adjust terminal size once shell is ready + ws.on('message', (msg) => { + try { + const input = JSON.parse(msg); + if (input.type === 'resize') { + const resizeCmd = resizeCommand(input.rows, input.cols); + stream.write(resizeCmd); // Resize the terminal in SSH + } else { + stream.write(msg); // Regular input handling + } + } catch (e) { + // If it's not JSON, it's a regular key press + stream.write(msg); + } + }); + + stream.on('data', (data) => { + console.log(`SSH Output: ${data}`); + ws.send(data.toString()); // Send the data back to the client once }); - // 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', (msg) => { - const input = JSON.parse(msg); - if (input.type === 'resize') { - const resizeCommand = `stty rows ${input.rows} cols ${input.cols}\n`; - stream.write(resizeCommand); - } else { - stream.write(input); - } - }); + // Send only the resize commands initially without `stty sane` + const initialResizeCmd = resizeCommand(24, 80); // Example initial size + stream.write(initialResizeCmd); // Set terminal size }); }).on('error', (err) => { console.log('SSH Connection Error: ', err); ws.send(`SSH Error: ${err}`); }).connect({ - host: data.host, // Host provided from the client - port: data.port, // Default SSH port - username: data.username, // Username provided from the client - password: data.password, // Password provided from the client - keepaliveInterval: 10000, // Send a heartbeat every 10 seconds - keepaliveCountMax: 5, // Allow three missed heartbeats before considering the connection dead + host: data.host, + port: data.port, + username: data.username, + password: data.password, + keepaliveInterval: 10000, + keepaliveCountMax: 5, }); } } 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 - } - } else { - console.error('SSH connection is not established yet.'); - } + console.log('Received non-JSON message: ', message); } }); - // Handle WebSocket close event ws.on('close', () => { - console.log('WebSocket closed'); - clearInterval(interval); if (conn) { - conn.end(); // Close SSH connection when WebSocket client disconnects + conn.end(); } + if (interval) { + clearInterval(interval); + } + console.log('WebSocket connection closed'); }); }); -// Start the WebSocket server on port 8081 +// Start HTTP server server.listen(8081, () => { - console.log('WebSocket server is listening on ws://localhost:8081'); + console.log('WebSocket server listening on ws://localhost:8081'); }); \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7528fecf..cf79b9bc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,7 +10,7 @@ const App = () => { const fitAddon = useRef(null); const socket = useRef(null); const [host, setHost] = useState(''); - const [port, setPort] = useState('22'); + const [port, setPort] = useState(22); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isConnected, setIsConnected] = useState(false); @@ -27,6 +27,8 @@ const App = () => { macOptionIsMeta: true, allowProposedApi: true, scrollback: 5000, + // Do not enable local echo + disableStdin: false, }); // Initialize and attach the fit addon to the terminal @@ -38,12 +40,6 @@ const App = () => { // Resize terminal to fit the container initially fitAddon.current.fit(); - terminal.current.onData((data) => { - if (socket.current && socket.current.readyState === WebSocket.OPEN) { - socket.current.send(data); - } - }); - // Adjust terminal size on window resize const handleResize = () => { fitAddon.current.fit(); @@ -73,7 +69,7 @@ const App = () => { return; } - socket.current = new WebSocket(wsUrl); + socket.current = new WebSocket("ws://localhost:8081"); socket.current.onopen = () => { terminal.current.writeln(`Connected to WebSocket server at ${wsUrl}`); @@ -91,6 +87,8 @@ const App = () => { }; socket.current.onmessage = (event) => { + // Write the incoming data from WebSocket to the terminal + // This ensures that data coming from the WebSocket server is shown in the terminal terminal.current.write(event.data); }; @@ -102,10 +100,27 @@ const App = () => { terminal.current.writeln('Disconnected from WebSocket server.'); setIsConnected(false); }; + + // Handle terminal input and send it over WebSocket + terminal.current.onData((data) => { + // Send input data over WebSocket without echoing it back to the terminal + if (socket.current && socket.current.readyState === WebSocket.OPEN) { + socket.current.send(data); // Only send to WebSocket, no echo + } + }); }; - const handleInputChange = (event, setState) => { - setState(event.target.value); + const handleInputChange = (event, setState, isNumber = false) => { + let value = event.target.value; + + if (isNumber) { + value = Number(value); // Convert to number if it's a number field + if (isNaN(value)) { + value = ''; // Optional: set an empty string if the input is invalid + } + } + + setState(value); // Set the state with the appropriate value }; const handleSideBarHiding = () => { @@ -114,7 +129,7 @@ const App = () => { return ( - + Connection Details { onChange={(e) => handleInputChange(e, setHost)} /> handleInputChange(e, setPort)} + onChange={(e) => handleInputChange(e, setPort, true)} />