Replace SocketIO with AJAX polling for terminal

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