Implement real terminal connection using SocketIO

parent 50e7ab06
......@@ -4,3 +4,4 @@ flask>=3.1
flask-login>=0.6
flask-sqlalchemy>=3.1
werkzeug>=3.1
flask-socketio>=5.0
\ No newline at end of file
......@@ -140,6 +140,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
......
......@@ -31,8 +31,9 @@
{% block scripts %}
<script>
let term = null;
let socket = null;
let connected = false;
let commandBuffer = '';
let requestId = null;
document.getElementById('connectBtn').addEventListener('click', connect);
document.getElementById('disconnectBtn').addEventListener('click', disconnect);
......@@ -48,37 +49,51 @@ function connect() {
if (!term) {
term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Connecting to ' + username + '@{{ client_id }}...\r\n');
}
// For demo purposes, we'll simulate the connection
// Initialize socket
if (!socket) {
socket = io();
}
term.write('Connecting to ' + username + '@{{ client_id }}...\r\n');
connected = true;
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
document.getElementById('sshUsername').disabled = true;
setTimeout(() => {
// Send connect_terminal
socket.emit('connect_terminal', {client_id: '{{ client_id }}', username: username});
// Handle terminal_connected
socket.on('terminal_connected', function(data) {
requestId = data.request_id;
term.write('Connected successfully!\r\n');
term.write('Welcome to {{ client_id }}\r\n');
term.write('$ ');
}, 1000);
});
// Handle terminal_data
socket.on('terminal_data', function(data) {
term.write(data.data);
});
// Handle terminal_error
socket.on('terminal_error', function(data) {
term.write('Error: ' + data.error + '\r\n');
disconnect();
});
// Handle terminal_closed
socket.on('terminal_closed', function(data) {
term.write('\r\nTerminal closed.\r\n');
disconnect();
});
// Handle input
term.onData(data => {
if (!connected) return;
if (data === '\r' || data === '\n') {
// Enter pressed, process command
processCommand(commandBuffer.trim());
commandBuffer = '';
} else if (data === '\x7f' || data === '\b') { // Backspace
if (commandBuffer.length > 0) {
commandBuffer = commandBuffer.slice(0, -1);
term.write('\b \b');
}
} else {
commandBuffer += data;
term.write(data);
}
if (!connected || !requestId) return;
socket.emit('terminal_data', {request_id: requestId, data: data});
});
}
......@@ -87,36 +102,15 @@ function disconnect() {
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
document.getElementById('sshUsername').disabled = false;
commandBuffer = '';
if (term) {
term.write('\r\nDisconnected.\r\n');
if (requestId) {
socket.emit('disconnect_terminal', {request_id: requestId});
requestId = null;
}
}
function processCommand(command) {
if (!connected || !term) return;
term.write('\r\n');
// Simulate command execution
setTimeout(() => {
if (command === 'ls') {
term.write('Desktop Documents Downloads Music Pictures Videos\r\n');
} else if (command === 'pwd') {
term.write('/home/' + document.getElementById('sshUsername').value + '\r\n');
} else if (command === 'whoami') {
term.write(document.getElementById('sshUsername').value + '\r\n');
} else if (command === 'exit' || command === 'logout') {
disconnect();
return;
} else if (command === '') {
// Empty command
} else {
term.write('Command not found: ' + command + '\r\n');
if (term) {
term.write('\r\nDisconnected.\r\n');
}
term.write('$ ');
}, 500);
}
// Focus on terminal when connected
......
......@@ -27,15 +27,19 @@ import json
import sys
import os
import threading
import uuid
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from flask_socketio import SocketIO, emit
# Client registry: id -> websocket
clients = {}
# Active tunnels: request_id -> {'client_ws': ws, 'wsssh_ws': ws, 'client_id': id}
active_tunnels = {}
# Active terminals: request_id -> {'web_sid': sid, 'client_id': id, 'username': username}
active_terminals = {}
debug = False
server_password = None
args = None
......@@ -52,6 +56,7 @@ db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
socketio = SocketIO(app)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
......@@ -174,6 +179,54 @@ def get_clients():
def logos_files(filename):
return send_from_directory('logos', filename)
@socketio.on('connect_terminal')
def handle_connect_terminal(data):
client_id = data['client_id']
username = data.get('username', 'root')
if client_id in clients:
request_id = str(uuid.uuid4())
active_terminals[request_id] = {'web_sid': request.sid, 'client_id': client_id, 'username': username}
asyncio.create_task(send_terminal_request(request_id, client_id, username))
else:
emit('terminal_error', {'error': 'Client not connected'})
async def send_terminal_request(request_id, client_id, username):
await clients[client_id].send(json.dumps({
"type": "terminal_request",
"request_id": request_id,
"username": username
}))
@socketio.on('terminal_data')
def handle_terminal_data(data):
request_id = data['request_id']
if request_id in active_terminals:
client_id = active_terminals[request_id]['client_id']
if client_id in clients:
asyncio.create_task(send_terminal_data(request_id, client_id, data['data']))
async def send_terminal_data(request_id, client_id, data):
await clients[client_id].send(json.dumps({
"type": "terminal_data",
"request_id": request_id,
"data": data
}))
@socketio.on('disconnect_terminal')
def handle_disconnect_terminal(data):
request_id = data['request_id']
if request_id in active_terminals:
client_id = active_terminals[request_id]['client_id']
if client_id in clients:
asyncio.create_task(send_terminal_close(request_id, client_id))
del active_terminals[request_id]
async def send_terminal_close(request_id, client_id):
await clients[client_id].send(json.dumps({
"type": "terminal_close",
"request_id": request_id
}))
async def handle_websocket(websocket, path=None):
try:
async for message in websocket:
......@@ -246,6 +299,19 @@ async def handle_websocket(websocket, path=None):
}))
# Clean up tunnel
del active_tunnels[request_id]
elif data.get('type') == 'terminal_ack':
request_id = data['request_id']
if request_id in active_terminals:
socketio.emit('terminal_connected', {'request_id': request_id}, to=active_terminals[request_id]['web_sid'])
elif data.get('type') == 'terminal_data':
request_id = data['request_id']
if request_id in active_terminals:
socketio.emit('terminal_data', {'data': data['data']}, to=active_terminals[request_id]['web_sid'])
elif data.get('type') == 'terminal_close':
request_id = data['request_id']
if request_id in active_terminals:
socketio.emit('terminal_closed', {'request_id': request_id}, to=active_terminals[request_id]['web_sid'])
del active_terminals[request_id]
except websockets.exceptions.ConnectionClosed:
# Remove from registry and clean up tunnels
disconnected_client = None
......@@ -322,7 +388,7 @@ async def main():
ssl_context = (web_cert_path, web_key_path)
def run_flask():
app.run(host=args.web_host, port=args.web_port, debug=debug, use_reloader=False, ssl_context=ssl_context)
socketio.run(app, host=args.web_host, port=args.web_port, debug=debug, use_reloader=False)
flask_thread = threading.Thread(target=run_flask, daemon=True)
flask_thread.start()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment