Replace SocketIO with AJAX polling for terminal

parent fd0dd8cd
......@@ -4,5 +4,3 @@ flask>=3.1
flask-login>=0.6
flask-sqlalchemy>=3.1
werkzeug>=3.1
\ No newline at end of file
flask-socketio>=5.0
eventlet>=0.33
\ No newline at end of file
......@@ -140,7 +140,6 @@
<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,9 +31,9 @@
{% block scripts %}
<script>
let term = null;
let socket = null;
let connected = false;
let requestId = null;
let pollInterval = null;
document.getElementById('connectBtn').addEventListener('click', connect);
document.getElementById('disconnectBtn').addEventListener('click', disconnect);
......@@ -51,11 +51,6 @@ function connect() {
term.open(document.getElementById('terminal'));
}
// Initialize socket
if (!socket) {
socket = io();
}
term.write('Connecting to ' + username + '@{{ client_id }}...\r\n');
connected = true;
......@@ -63,37 +58,41 @@ function connect() {
document.getElementById('disconnectBtn').disabled = false;
document.getElementById('sshUsername').disabled = true;
// Send connect_terminal
socket.emit('connect_terminal', {client_id: '{{ client_id }}', username: username});
// Handle terminal_connected
socket.on('terminal_connected', function(data) {
// Send connect request
fetch('/terminal/{{ client_id }}/connect', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'username=' + encodeURIComponent(username)
})
.then(response => response.json())
.then(data => {
if (data.request_id) {
requestId = data.request_id;
term.write('Connected successfully!\r\n');
term.write('$ ');
});
// Handle terminal_data
socket.on('terminal_data', function(data) {
term.write(data.data);
});
// Handle terminal_error
socket.on('terminal_error', function(data) {
term.write('Connected successfully!\r\n$ ');
// Start polling for data
pollInterval = setInterval(pollData, 500);
} else {
term.write('Error: ' + data.error + '\r\n');
disconnect();
});
// Handle terminal_closed
socket.on('terminal_closed', function(data) {
term.write('\r\nTerminal closed.\r\n');
}
})
.catch(error => {
term.write('Connection failed: ' + error + '\r\n');
disconnect();
});
// Handle input
term.onData(data => {
if (!connected || !requestId) return;
socket.emit('terminal_data', {request_id: requestId, data: data});
fetch('/terminal/{{ client_id }}/data', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId) + '&data=' + encodeURIComponent(data)
});
});
}
......@@ -103,8 +102,19 @@ function disconnect() {
document.getElementById('disconnectBtn').disabled = true;
document.getElementById('sshUsername').disabled = false;
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
if (requestId) {
socket.emit('disconnect_terminal', {request_id: requestId});
fetch('/terminal/{{ client_id }}/disconnect', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'request_id=' + encodeURIComponent(requestId)
});
requestId = null;
}
......@@ -113,6 +123,17 @@ function disconnect() {
}
}
function pollData() {
if (!requestId) return;
fetch('/terminal/{{ client_id }}/data?request_id=' + encodeURIComponent(requestId))
.then(response => response.text())
.then(data => {
if (data) {
term.write(data);
}
});
}
// Focus on terminal when connected
document.addEventListener('keydown', function(e) {
if (connected && term) {
......
......@@ -32,7 +32,6 @@ from flask import Flask, render_template, request, redirect, url_for, flash, jso
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 = {}
......@@ -56,7 +55,6 @@ db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
socketio = SocketIO(app, async_mode='eventlet')
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
......@@ -179,16 +177,16 @@ 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')
@app.route('/terminal/<client_id>/connect', methods=['POST'])
@login_required
def connect_terminal(client_id):
username = request.form.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}
active_terminals[request_id] = {'client_id': client_id, 'username': username, 'data': []}
asyncio.create_task(send_terminal_request(request_id, client_id, username))
else:
emit('terminal_error', {'error': 'Client not connected'})
return jsonify({'request_id': request_id})
return jsonify({'error': 'Client not connected'}), 400
async def send_terminal_request(request_id, client_id, username):
await clients[client_id].send(json.dumps({
......@@ -197,13 +195,22 @@ async def send_terminal_request(request_id, client_id, username):
"username": username
}))
@socketio.on('terminal_data')
def handle_terminal_data(data):
request_id = data['request_id']
@app.route('/terminal/<client_id>/data', methods=['GET', 'POST'])
@login_required
def terminal_data(client_id):
if request.method == 'POST':
request_id = request.form.get('request_id')
data = request.form.get('data')
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']))
asyncio.create_task(send_terminal_data(request_id, active_terminals[request_id]['client_id'], data))
return 'OK'
else:
request_id = request.args.get('request_id')
if request_id in active_terminals:
data = active_terminals[request_id]['data']
active_terminals[request_id]['data'] = []
return ''.join(data)
return ''
async def send_terminal_data(request_id, client_id, data):
await clients[client_id].send(json.dumps({
......@@ -212,14 +219,14 @@ async def send_terminal_data(request_id, client_id, data):
"data": data
}))
@socketio.on('disconnect_terminal')
def handle_disconnect_terminal(data):
request_id = data['request_id']
@app.route('/terminal/<client_id>/disconnect', methods=['POST'])
@login_required
def disconnect_terminal(client_id):
request_id = request.form.get('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))
asyncio.create_task(send_terminal_close(request_id, active_terminals[request_id]['client_id']))
del active_terminals[request_id]
return 'OK'
async def send_terminal_close(request_id, client_id):
await clients[client_id].send(json.dumps({
......@@ -302,15 +309,15 @@ async def handle_websocket(websocket, path=None):
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'])
active_terminals[request_id]['data'].append('\r\nConnected successfully!\r\n$ ')
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'])
active_terminals[request_id]['data'].append(data['data'])
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'])
active_terminals[request_id]['data'].append('\r\nTerminal closed.\r\n')
del active_terminals[request_id]
except websockets.exceptions.ConnectionClosed:
# Remove from registry and clean up tunnels
......@@ -388,7 +395,7 @@ async def main():
ssl_context = (web_cert_path, web_key_path)
def run_flask():
socketio.run(app, host=args.web_host, port=args.web_port, debug=debug, use_reloader=False)
app.run(host=args.web_host, port=args.web_port, debug=debug, use_reloader=False, threaded=True)
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