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/<base>/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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-07 15:01:09 +10:00
parent dadda8765d
commit 55d1686d8d
5 changed files with 29 additions and 11 deletions

View File

@ -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';

View File

@ -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<relayRows.length;i++){ var el=relayRows[i]; if(!el) continue }

View File

@ -310,6 +310,16 @@
// --- Export ---
window.bls = {
createChannel: function(name){ return new Channel(name); },
// where's the relay? bundled server.py = ws://host:8787; hosted behind a
// reverse proxy (https and/or subpath) = same-origin <base>/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,

View File

@ -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){

View File

@ -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: