From 55d1686d8df83b04e88ae70c30ccb57f468bca18 Mon Sep 17 00:00:00 2001 From: type-two Date: Tue, 7 Jul 2026 15:01:09 +1000 Subject: [PATCH] Support hosting behind a reverse proxy (https/subpath) BLS.relayUrl() picks the relay endpoint: bundled server.py keeps ws://host:8787; served over https or from a subpath it becomes same-origin wss://host//ws. Patient/join links are now path-relative so subpath hosting works. server.py gains --bind and --http-port for reverse-proxy setups where 8000 is taken. Co-Authored-By: Claude Fable 5 --- easy.html | 4 ++-- js/admin.js | 7 ++++--- js/bls.js | 10 ++++++++++ js/patient.js | 2 +- server.py | 17 ++++++++++++----- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/easy.html b/easy.html index b672463..364fd25 100644 --- a/easy.html +++ b/easy.html @@ -146,7 +146,7 @@ } function connect(){ var ws; - try{ ws = new WebSocket('ws://' + location.hostname + ':8787') }catch(_){ return } + try{ ws = new WebSocket(BLS.relayUrl()) }catch(_){ return } ws.onopen = function(){ net.ws = ws; try{ ws.send(JSON.stringify({type:'join', sessionId: net.sid, role:'admin'})) }catch(_){ } @@ -253,7 +253,7 @@ window.open('patient.html' + (net.sid ? '#' + net.sid : ''), 'bls-patient'); }); document.getElementById('copyLinkBig').addEventListener('click', function(){ - var link = location.origin + '/patient.html#' + net.sid; + var link = new URL('patient.html#' + net.sid, location.href).href; var b = this; try{ navigator.clipboard.writeText(link) }catch(_){ try{ prompt('Patient link:', link) }catch(__){ } } b.textContent = '✅ Copied — open it on the patient device'; diff --git a/js/admin.js b/js/admin.js index 195355f..322993b 100644 --- a/js/admin.js +++ b/js/admin.js @@ -439,17 +439,18 @@ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded else if(wsConnected && ws){ ws.send(JSON.stringify(payload)) } }catch(e){} } - function updateJoinLink(){ var sid = (wsSession.value||'').trim(); var base = location.origin + '/patient.html'; if(sid){ joinLink.value = base + '#' + encodeURIComponent(sid) } else { joinLink.value = '' } } + function updateJoinLink(){ var sid = (wsSession.value||'').trim(); var base = new URL('patient.html', location.href).href; if(sid){ joinLink.value = base + '#' + encodeURIComponent(sid) } else { joinLink.value = '' } } function renderOsHelp(){ var ua = navigator.userAgent||''; var isWin = /Windows/i.test(ua); var isMac = /Macintosh|Mac OS/i.test(ua); var start = (isWin? 'start-relay.bat' : isMac? 'start-relay.command' : './start-relay.sh'); var open = (isWin? 'double-click' : isMac? 'double-click' : 'run'); return { setup: start, start: start, open: open } } (function(){ var el = document.getElementById('osHelp'); if(!el) return; var h = renderOsHelp(); var text = 'Start the server: ' + h.open + ' ' + h.start + ' — nothing to install, first time or any time'; el.innerText = text })(); function copyText(t){ var ok=false; if(navigator.clipboard && navigator.clipboard.writeText){ try{ navigator.clipboard.writeText(t); ok=true }catch(e){} } if(!ok){ var ta=document.createElement('textarea'); ta.value=t; ta.setAttribute('readonly',''); ta.style.position='fixed'; ta.style.bottom='0'; ta.style.left='0'; ta.style.opacity='0.01'; document.body.appendChild(ta); ta.focus(); ta.select(); try{ ok=document.execCommand('copy') }catch(e){} document.body.removeChild(ta) } if(!ok){ try{ alert(t) }catch(_){ } } return ok } function setRelayStatus(on){ if(relayStatusDot){ relayStatusDot.classList.toggle('on', !!on) } if(relayStatusText){ relayStatusText.textContent = on? 'Running' : 'Not running' } } (function(){ var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = true } })(); + (function(){ if(wsUrl && !(wsUrl.value||'').trim()){ wsUrl.value = BLS.relayUrl() } })(); (function(){ var pollMs = 3000; function schedule(){ setTimeout(run, pollMs) } function run(){ checkRelayOnce().then(function(ok){ setRelayStatus(!!ok); var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = !ok } pollMs = ok ? 3000 : Math.min(Math.round(pollMs*1.5), 30000); schedule() }).catch(function(){ setRelayStatus(false); var btn = document.getElementById('wsConnect'); if(btn){ btn.disabled = true } pollMs = Math.min(Math.round(pollMs*1.5), 30000); schedule() }) } - async function checkRelayOnce(){ var host = location.hostname || 'localhost'; var url = 'ws://' + host + ':8787'; try{ return await new Promise(function(resolve){ var temp = new WebSocket(url); var done=false; var timer = setTimeout(function(){ if(!done){ resolve(false); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } } }, 1500); + async function checkRelayOnce(){ var url = BLS.relayUrl(); try{ return await new Promise(function(resolve){ var temp = new WebSocket(url); var done=false; var timer = setTimeout(function(){ if(!done){ resolve(false); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } } }, 1500); temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'ping'})) }catch(e){} }; temp.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='pong'){ done=true; clearTimeout(timer); if(temp && temp.readyState===1){ try{ temp.close() }catch(e){} } resolve(true); return } }catch(e){} resolve(false) }; temp.onerror = function(){ resolve(false) }; @@ -458,7 +459,7 @@ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded run() })(); if(startRelayBtn){ startRelayBtn.addEventListener('click', function(){ var h = renderOsHelp(); var ok = copyText(h.start); var old = startRelayBtn.textContent; startRelayBtn.textContent = ok? 'Copied!' : 'Copy Start Command'; setTimeout(function(){ startRelayBtn.textContent = old }, 1200) }) } - if(stopRelayBtn){ stopRelayBtn.addEventListener('click', function(){ var host = location.hostname || 'localhost'; var url = 'ws://' + host + ':8787'; try{ var temp = new WebSocket(url); temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'shutdown'})) }catch(e){} }; setTimeout(function(){ setRelayStatus(false) }, 1500) }catch(e){} }) } + if(stopRelayBtn){ stopRelayBtn.addEventListener('click', function(){ var url = BLS.relayUrl(); try{ var temp = new WebSocket(url); temp.onopen = function(){ try{ temp.send(JSON.stringify({type:'shutdown'})) }catch(e){} }; setTimeout(function(){ setRelayStatus(false) }, 1500) }catch(e){} }) } function wsConnect(){ var url = (wsUrl.value||'').trim(); var sid = (wsSession.value||'').trim(); if(!sid){ sid = randSid(); wsSession.value = sid; updateJoinLink() } var btn = document.getElementById('wsConnect'); if(btn && btn.disabled){ wsStatus.textContent = 'Relay not running'; return } if(ws){ try{ ws.close() }catch(e){} ws=null } if(!url){ wsStatus.textContent = 'Missing URL'; return } try{ ws = new WebSocket(url) }catch(e){ wsStatus.textContent = 'Error'; return } wsSid = sid; wsStatus.textContent = 'Connecting...'; ws.onopen = function(){ wsConnected = true; wsStatus.textContent = 'Connected'; try{ ws.send(JSON.stringify({type:'join', sessionId:sid, role:'admin'})) }catch(e){} publishState() }; ws.onclose = function(){ wsConnected = false; wsStatus.textContent = 'Disconnected' }; ws.onerror = function(){ wsStatus.textContent = 'Error' }; ws.onmessage = function(ev){ try{ var msg = JSON.parse(ev.data); if(msg && msg.type==='joined'){ wsStatus.textContent = 'Connected' } else if(msg && msg.type==='patientViewport'){ var pv = msg.payload||{}; try{ window.PatientViewport = pv }catch(_){ } var r = window.AdminRenderer; if(r){ r.resize(parseInt(pv.canvasW||640,10), parseInt(pv.canvasH||360,10)) } var pvShort = document.getElementById('patientViewportShort'); if(pvShort){ pvShort.textContent = (pv.canvasW||'?')+'×'+(pv.canvasH||'?') } } }catch(e){} } } function wsDisconnect(){ if(ws){ try{ ws.close() }catch(e){} } ws=null; wsConnected=false; wsStatus.textContent = 'Disconnected' } function refreshNetMode(){ var m = (netModeSel && netModeSel.value) || 'relay'; if(rtcControls) rtcControls.style.display = (m==='webrtc'? 'flex':'none'); var relayRows = document.querySelectorAll('#osHelp, #wsUrl, #wsSession, #detectIP, #wsConnect, #wsDisconnect'); for(var i=0;i/ws + relayUrl: function(){ + if(!/^https?:$/.test(location.protocol)) return 'ws://localhost:8787'; + var base = location.pathname.replace(/[^/]*$/, ''); + if(location.protocol === 'https:' || base !== '/'){ + return (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + base + 'ws'; + } + return 'ws://' + (location.hostname || 'localhost') + ':8787'; + }, defaultState: defaultState, loadState: loadState, saveState: saveState, diff --git a/js/patient.js b/js/patient.js index 11523b3..dba6f50 100644 --- a/js/patient.js +++ b/js/patient.js @@ -160,7 +160,7 @@ var qs = new URLSearchParams(location.search); var hashSid = (location.hash || '').replace('#', ''); var wsSid = qs.get('sid') || hashSid || 'default'; - var wsUrl = qs.get('url') || ('ws://' + (location.hostname || 'localhost') + ':8787'); + var wsUrl = qs.get('url') || BLS.relayUrl(); var rtcMode = !!qs.get('p2p'); if(!rtcMode && wsUrl){ diff --git a/server.py b/server.py index 417e984..c2d6bec 100644 --- a/server.py +++ b/server.py @@ -213,27 +213,34 @@ class _QuietHTTP(SimpleHTTPRequestHandler): pass +def _argval(flag, default): + args = sys.argv[1:] + return args[args.index(flag) + 1] if flag in args else default + + def main(): os.chdir(os.path.dirname(os.path.abspath(__file__))) + bind = _argval('--bind', '0.0.0.0') # e.g. a tailnet IP behind a reverse proxy + http_port = int(_argval('--http-port', HTTP_PORT)) socketserver.ThreadingTCPServer.allow_reuse_address = True ThreadingHTTPServer.allow_reuse_address = True - httpd = ThreadingHTTPServer(('0.0.0.0', HTTP_PORT), _QuietHTTP) - wsd = socketserver.ThreadingTCPServer(('0.0.0.0', WS_PORT), WSHandler) + httpd = ThreadingHTTPServer((bind, http_port), _QuietHTTP) + wsd = socketserver.ThreadingTCPServer((bind, WS_PORT), WSHandler) httpd.daemon_threads = True wsd.daemon_threads = True _SERVERS.extend([httpd, wsd]) - ip = local_ip() + ip = bind if bind not in ('0.0.0.0', '') else local_ip() print('backnforth running:') - print(' Therapist page: http://%s:%d/' % (ip, HTTP_PORT)) + print(' Therapist page: http://%s:%d/' % (ip, http_port)) print(' Session relay: ws://%s:%d' % (ip, WS_PORT)) print('Press Ctrl+C to stop.') threading.Thread(target=wsd.serve_forever, daemon=True).start() if '--no-browser' not in sys.argv: try: - webbrowser.open('http://%s:%d/easy.html' % (ip, HTTP_PORT)) + webbrowser.open('http://%s:%d/easy.html' % (ip, http_port)) except Exception: pass try: