From ccdfdc97e75022374a3706299a9344530de083a8 Mon Sep 17 00:00:00 2001 From: Junhao Liao Date: Fri, 10 Jun 2022 13:55:11 -0400 Subject: [PATCH] [Security] enable SSL for all servers; add local auth for websockify server; clean up websockify server after disconnection --- application/features/Audio.py | 10 +++++---- application/features/Term.py | 9 ++++---- application/features/VNC.py | 18 +++++++++------- application/features/mywebsockify.py | 23 ++++++++++++++++++++ client/package.json | 2 +- client/src/actions/audio.js | 4 ++-- client/src/actions/term.js | 4 ++-- client/src/actions/vnc.js | 5 ++--- desktop_client/main.js | 32 +++++++++++++++++++++------- ictrl_be.py | 2 +- 10 files changed, 76 insertions(+), 33 deletions(-) create mode 100644 application/features/mywebsockify.py diff --git a/application/features/Audio.py b/application/features/Audio.py index 7b723a1f..4aa7efd6 100644 --- a/application/features/Audio.py +++ b/application/features/Audio.py @@ -25,7 +25,8 @@ from typing import Optional import paramiko -from SimpleWebSocketServer import SimpleSSLWebSocketServer, WebSocket, SimpleWebSocketServer +from SimpleWebSocketServer import SimpleSSLWebSocketServer, WebSocket +from werkzeug.serving import generate_adhoc_ssl_context from .Connection import Connection from .. import app @@ -37,6 +38,7 @@ TRY_FFMPEG_MAX_COUNT = 3 AUDIO_BUFFER_SIZE = 20480 + class Audio(Connection): def __init__(self): self.id = None @@ -167,9 +169,9 @@ def handleClose(self): print("AUDIO_PORT =", AUDIO_PORT) if os.environ.get('SSL_CERT_PATH') is None: - # no certificate provided, run in non-encrypted mode - # FIXME: consider using a self-signing certificate for local connections - audio_server = SimpleWebSocketServer('', AUDIO_PORT, AudioWebSocket) + # no certificate provided, generate self-signing certificate + audio_server = SimpleSSLWebSocketServer('', AUDIO_PORT, AudioWebSocket, + ssl_context=generate_adhoc_ssl_context()) else: import ssl diff --git a/application/features/Term.py b/application/features/Term.py index 9da78eae..c78695b8 100644 --- a/application/features/Term.py +++ b/application/features/Term.py @@ -23,8 +23,9 @@ import uuid from typing import Optional -from SimpleWebSocketServer import SimpleSSLWebSocketServer, WebSocket, SimpleWebSocketServer +from SimpleWebSocketServer import SimpleSSLWebSocketServer, WebSocket from paramiko import Channel +from werkzeug.serving import generate_adhoc_ssl_context from .Connection import Connection from .. import app @@ -119,9 +120,9 @@ def handleClose(self): print("TERMINAL_PORT =", TERMINAL_PORT) if os.environ.get('SSL_CERT_PATH') is None: - # no certificate provided, run in non-encrypted mode - # FIXME: consider using a self-signing certificate for local connections - terminal_server = SimpleWebSocketServer('', TERMINAL_PORT, TermWebSocket) + # no certificate provided, generate self-signing certificate + terminal_server = SimpleSSLWebSocketServer('', TERMINAL_PORT, TermWebSocket, + ssl_context=generate_adhoc_ssl_context()) else: import ssl diff --git a/application/features/VNC.py b/application/features/VNC.py index d80c0281..1bf33cef 100644 --- a/application/features/VNC.py +++ b/application/features/VNC.py @@ -22,21 +22,23 @@ import re import threading -import websockify - from .Connection import Connection +from .mywebsockify import MyProxyRequestHandler, MySSLProxyServer from .vncpasswd import decrypt_passwd, obfuscate_password from ..utils import find_free_port def websocket_proxy_thread(local_websocket_port, local_vnc_port): if os.environ.get('SSL_CERT_PATH') is None: - # no certificate provided, run in non-encrypted mode - # FIXME: consider using a self-signing certificate for local connections - proxy_server = websockify.LibProxyServer(listen_port=local_websocket_port, target_host='', - target_port=local_vnc_port, - run_once=True) - proxy_server.serve_forever() + proxy_server = MySSLProxyServer(RequestHandlerClass=MyProxyRequestHandler, + listen_port=local_websocket_port, target_host='', + target_port=local_vnc_port) + + # only serve two request: + # 1st: first handshake: upgrade the HTTP request + # 2nd: actually serve the ws connection + for _ in range(2): + proxy_server.handle_request() proxy_server.server_close() else: import subprocess diff --git a/application/features/mywebsockify.py b/application/features/mywebsockify.py new file mode 100644 index 00000000..1432e1e4 --- /dev/null +++ b/application/features/mywebsockify.py @@ -0,0 +1,23 @@ +import websockify +from werkzeug.serving import generate_adhoc_ssl_context + +from application.utils import local_auth + + +class MyProxyRequestHandler(websockify.ProxyRequestHandler): + def auth_connection(self): + super(MyProxyRequestHandler, self).auth_connection() + if not local_auth(headers=self.headers, abort_func=self.server.server_close): + # local auth failure + return + + +class MySSLProxyServer(websockify.LibProxyServer): + # noinspection PyPep8Naming + def __init__(self, RequestHandlerClass=websockify.ProxyRequestHandler, ssl_context=None, **kwargs): + super(MySSLProxyServer, self).__init__(RequestHandlerClass=RequestHandlerClass, **kwargs) + + if ssl_context is None: + # no certificate provided, generate self-signing certificate + ssl_context = generate_adhoc_ssl_context() + self.socket = ssl_context.wrap_socket(self.socket, server_side=True) diff --git a/client/package.json b/client/package.json index 5d84159b..e32e3b95 100644 --- a/client/package.json +++ b/client/package.json @@ -51,6 +51,6 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:5000", + "proxy": "https://localhost:5000", "//": "https://ictrl.ca" } diff --git a/client/src/actions/audio.js b/client/src/actions/audio.js index d694442b..8189093f 100644 --- a/client/src/actions/audio.js +++ b/client/src/actions/audio.js @@ -36,8 +36,8 @@ export const launch_audio = (vncsd, sessionID) => { }).then((response) => { const {port, audio_id} = response.data; const socket = new WebSocket( - `${process.env.REACT_APP_DOMAIN_NAME ? 'wss' : 'ws'} -://${process.env.REACT_APP_DOMAIN_NAME || '127.0.0.1'}:${port}/${audio_id}`); + `wss://${process.env.REACT_APP_DOMAIN_NAME || + '127.0.0.1'}:${port}/${audio_id}`); socket.binaryType = 'arraybuffer'; socket.onopen = (_) => { diff --git a/client/src/actions/term.js b/client/src/actions/term.js index b0f3fa8c..adaff28c 100644 --- a/client/src/actions/term.js +++ b/client/src/actions/term.js @@ -611,8 +611,8 @@ const setupWebGL = (term) => { const setupWebSocket = (term, term_id, port) => { const socket = new WebSocket( - `${process.env.REACT_APP_DOMAIN_NAME ? 'wss' : 'ws'} -://${process.env.REACT_APP_DOMAIN_NAME || '127.0.0.1'}:${port}/${term_id}`); + `wss://${process.env.REACT_APP_DOMAIN_NAME || + '127.0.0.1'}:${port}/${term_id}`); socket.onopen = (_) => { const attachAddon = new AttachAddon(socket); diff --git a/client/src/actions/vnc.js b/client/src/actions/vnc.js index f6a90f8e..f3546bd0 100644 --- a/client/src/actions/vnc.js +++ b/client/src/actions/vnc.js @@ -33,8 +33,8 @@ import {htmlResponseToReason, isIOS} from './utils'; const setupDOM = (port, passwd) => { /* Creating a new RFB object and start a new connection */ - const url = `${process.env.REACT_APP_DOMAIN_NAME ? 'wss' : 'ws'} -://${process.env.REACT_APP_DOMAIN_NAME || '127.0.0.1'}:${port}`; + const url = `wss://${process.env.REACT_APP_DOMAIN_NAME || + '127.0.0.1'}:${port}`; const rfb = passwd ? new RFB( document.getElementById('screen'), @@ -257,7 +257,6 @@ export const vncConnect = async (vncViewer) => { // hide the Loading element vncViewer.setState({ loading: false, - disconnected: false, // sometimes 'disconnect' (below) is fired before 'connect' for unknown reasons }); }); diff --git a/desktop_client/main.js b/desktop_client/main.js index edd27468..1c054dc4 100644 --- a/desktop_client/main.js +++ b/desktop_client/main.js @@ -9,11 +9,7 @@ if (handleSquirrelEvent()) { } // Modules to control application life and create native browser window -const {app, BrowserWindow, Menu, MenuItem, ipcMain, session} = require( - 'electron'); -const {randomUUID} = require('crypto'); -const {spawn} = require('child_process'); -const {resolve} = require('path'); +const {app} = require('electron'); const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { @@ -23,8 +19,13 @@ if (!gotTheLock) { process.exit(); } +const {BrowserWindow, Menu, MenuItem, ipcMain, session} = require('electron'); +const {randomUUID} = require('crypto'); +const {spawn} = require('child_process'); +const {resolve} = require('path'); const {getFreePort, humanFileSize} = require('./utils'); const ProgressBar = require('./ProgressBar'); + const mainPort = getFreePort(); const localAuthKey = randomUUID(); @@ -65,7 +66,7 @@ if (isMac) { const setupNewWindowIcon = (url, newWindow) => { const {nativeImage} = require('electron'); - const {get} = require('http'); + const {get} = require('https'); const url_split = url.split('/'); const sessionId = url_split[url_split.length - 1]; @@ -75,6 +76,7 @@ const setupNewWindowIcon = (url, newWindow) => { port: mainPort, path: `/api/favicon/${feature}/${sessionId}`, headers: {'Authorization': `Bearer ${localAuthKey}`}, + rejectUnauthorized: false, }, (msg) => { const result = []; @@ -86,6 +88,8 @@ const setupNewWindowIcon = (url, newWindow) => { const icon = nativeImage.createFromBuffer(Buffer.concat(result)); newWindow.setIcon(icon); }); + }).on('error', (e) => { + console.error(e); }); }; @@ -120,7 +124,7 @@ let mainWindow = null; const setupLocalAuth = () => { // Modify the user agent for all requests to the following urls. const filter = { - urls: ['http://127.0.0.1/*', 'ws://127.0.0.1/*'], + urls: ['https://127.0.0.1/*', 'wss://127.0.0.1/*'], }; session.defaultSession.webRequest.onBeforeSendHeaders(filter, @@ -128,6 +132,18 @@ const setupLocalAuth = () => { details.requestHeaders['Authorization'] = `Bearer ${localAuthKey}`; callback({requestHeaders: details.requestHeaders}); }); + + app.on('certificate-error', + (event, webContents, url, error, certificate, callback) => { + if (url.startsWith('https://127.0.0.1') || + url.startsWith('wss://127.0.0.1')) { + event.preventDefault(); + callback(true); + } else { + console.error('Certificate Error at', url); + app.quit(); + } + }); }; const createDashboardWindow = () => { @@ -137,7 +153,7 @@ const createDashboardWindow = () => { // load dashboard mainWindow.setTitle('Loading... '); - mainWindow.loadURL(`http://127.0.0.1:${mainPort}/dashboard`); + mainWindow.loadURL(`https://127.0.0.1:${mainPort}/dashboard`); // need to reload on Mac because the first load times out very quickly mainWindow.webContents.on('did-fail-load', () => { mainWindow.reload(); diff --git a/ictrl_be.py b/ictrl_be.py index 814fa511..5ca4122c 100644 --- a/ictrl_be.py +++ b/ictrl_be.py @@ -43,4 +43,4 @@ def serve(path): return send_from_directory(app.static_folder, 'index.html') app.register_blueprint(api, url_prefix='/api') - app.run(host=APP_HOST, port=APP_PORT) + app.run(host=APP_HOST, port=APP_PORT, ssl_context="adhoc")