1

Our app works as follows:

  • Multiple remote machines connect to a central server using SSH connections
  • The central server has various Python processes which process data sent over these SSH connections and store it in a database
  • The central server also runs a bunch of Flask-based HTTP servers which process user requests and respond with data extracted from the DB; this forms the backend for the website which users access

We have been asked to add support for in-browser VNC connections to the remote machines to the app. This will require the user's browser to connect with a service running on the remote machines via a websocket. The way this would work is:

  1. The remote machine, let's say foo, would run the VNC service, which will listen on a specific port expecting a websocket connection.
  2. When foo connects to the central server via SSH, this socket will be forwarded, using ssh -R, to a socket file on the central server, let's say /remote/vnc_websocket/foo/websocket.sock
  3. When a user, let's call them Alice, want to VNC into machine foo, their browser sends a websocket request to central.server/vnc/foo
  4. The /vnc/{machine} route in our Flask app processes the request:
    • It checks the login token, sent in the request header, to confirm Alice is logged in
    • It accesses the DB to confirm that machine foo is connected, and that Alice has permission to access it
  5. Assuming all is well, the flask app then connects the incoming request from Alice to /remote/vnc_websocket/foo/websocket.sock, forwarding all traffic both ways

I know how to handle steps 1-4, but I have no idea how to implement step 5. I've looked at Flask-SockerIO and Flask-Websockets, but they're both focussed on implementing a Websocket server; I just want to connect a request to an existing server and then get out of the way.

I would prefer to integrate this solution into our existing Flask-based microservices, since we already have the code to handle things like checking logins, but if absolutely necessary I could implement a separate non-Flask-based microservice just for handling websocket requests. It would still have to be Python, though.

1 Answer 1

1

You can use Flask-SocketIO to handle WebSocket connections within your Flask app.

from flask import Flask as fl
from flask import request as rq
from flask_socketio import SocketIO, emit
import websockets
import asyncio
import threading
import json
import os

app = fl(__name__)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')

# Store active connections
active_connections = {}

def authenticated_websocket(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = rq.args.get('token') or rq.headers.get('Authorization')
        if not validate_token(token):
            return False
        return f(*args, **kwargs)
    return decorated_function

@socketio.on('connect')
@authenticated_websocket
def handle_connect():
    machine_name = rq.args.get('machine')
    
    if not user_has_access(rq.user, machine_name):
        return False
    
    # Start the bidirectional proxy in a separate thread
    connection_id = rq.sid
    proxy_thread = threading.Thread(
        target=start_bidirectional_proxy,
        args=(connection_id, machine_name, rq.sid)
    )
    proxy_thread.daemon = True
    proxy_thread.start()
    
    return True

@socketio.on('message')
def handle_message(data):
    connection_id = rq.sid
    if connection_id in active_connections:
        # This will be used to send data from client to remote
        active_connections[connection_id]['client_to_remote_queue'].put(data)

@socketio.on('disconnect')
def handle_disconnect():
    connection_id = rq.sid
    if connection_id in active_connections:
        active_connections[connection_id]['active'] = False
        # Clean up will happen in the proxy thread

def start_bidirectional_proxy(connection_id, machine_name, socketio_sid):
    # Runs in a separate thread to handle bidirectional forwarding
    
    # Create a new event loop for this thread
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    
    try:
        loop.run_until_complete(bidirectional_proxy(connection_id, machine_name, socketio_sid))
    finally:
        loop.close()

async def bidirectional_proxy(connection_id, machine_name, socketio_sid):
    socket_path = f"/remote/vnc_websocket/{machine_name}/websocket.sock"
    
    try:
        # Connect to the remote UNIX socket
        remote_ws = await websockets.unix_connect(socket_path)
        
        # Store connection info
        active_connections[connection_id] = {
            'remote_websocket': remote_ws,
            'active': True,
            'client_to_remote_queue': asyncio.Queue(),
            'socketio_sid': socketio_sid
        }
        
        print(f"Connected to remote socket for machine {machine_name}")
        
        # Start bidirectional forwarding tasks
        await asyncio.gather(
            forward_client_to_remote(connection_id),
            forward_remote_to_client(connection_id),
            return_exceptions=True
        )
        
    except Exception as e:
        print(f"Failed to establish proxy for {machine_name}: {e}")
        # Notify the client via SocketIO
        socketio.emit('error', {'message': 'Failed to connect to remote machine'}, room=socketio_sid)
    finally:
        # Cleanup
        if connection_id in active_connections:
            connection = active_connections.pop(connection_id)
            try:
                await connection['remote_websocket'].close()
            except:
                pass

async def forward_client_to_remote(connection_id):
    # Forward messages from client (browser) to remote machine
    connection = active_connections.get(connection_id)
    if not connection:
        return
    
    try:
        while connection['active']:
            try:
                # Wait for message from client with timeout
                message = await asyncio.wait_for(
                    connection['client_to_remote_queue'].get(), 
                    timeout=1.0
                )
                
                # Forward to remote machine
                if isinstance(message, dict):
                    message = json.dumps(message)
                
                await connection['remote_websocket'].send(message)
                connection['client_to_remote_queue'].task_done()
                
            except asyncio.TimeoutError:
                # Check if we should still be active
                continue
            except asyncio.CancelledError:
                break
                
    except Exception as e:
        print(f"Error in client-to-remote forwarding: {e}")

async def forward_remote_to_client(connection_id):
    # Forward messages from remote machine to client (browser)
    connection = active_connections.get(connection_id)
    if not connection:
        return
    
    try:
        async for message in connection['remote_websocket']:
            if not connection['active']:
                break
                
            # Forward to client via SocketIO
            socketio.emit('message', 
                         message, 
                         room=connection['socketio_sid'])
            
    except Exception as e:
        print(f"Error in remote-to-client forwarding: {e}")
    finally:
        connection['active'] = False
Sign up to request clarification or add additional context in comments.

2 Comments

Hrm. So you're proposing that, rather than connect the incoming websocket connection to the machine's forwarded websocket directly, I establish a separate websocket connection from the server to the machine, and then create my own websocket server to which the end user connects that takes every message it receives and forwards it to the other connection? What about data going the other way?
Pl check now. i was bit confused. Given the edited solution which gives a true bidirectional bridge

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.