+ backnforth
+ Bilateral stimulation for therapy β on whatever screen you already have.
+
+
+

+
What is this?
+
A gentle moving dot (and optional sound) that a patient follows with
+ their eyes while you guide the session. It runs in any web browser β
+ an old laptop, a second-hand phone, anything with a screen.
+
+
+
+

+
1Open it
+
Open the site on your computer. You'll see one big green
+ START button and a few large sliders. That's the whole
+ cockpit.
+
+
+
+

+
2Give the patient a screen
+
Easiest: tap Open patient screen and turn that window
+ to face them. Or tap Copy link and open it on their
+ phone or tablet β the two screens stay in sync by themselves.
+
+
+
+

+
3Press start
+
The dot glides left and right. The patient follows it with their
+ eyes while you lead the session. Sound can tick from side to side too
+ if you want it.
+
+
+
+

+
4Make it comfortable
+
Slide between turtle and rabbit until the pace feels right. Change
+ the size, the color, the sound β the patient's screen updates
+ instantly.
+
+
+
+

+
β οΈ Go gently
+
Ask about seizure history before showing moving or pulsing visuals.
+ Bilateral stimulation can bring up strong feelings β this tool assists
+ a trained clinician, it isn't therapy by itself. Pause any time; the
+ stop button is always one tap away.
+
+
+ Open backnforth β
+ Free and open source (MIT). Patient data never leaves
+ your device.
+
+
+
diff --git a/server.py b/server.py
index c2d6bec..a50ab05 100644
--- a/server.py
+++ b/server.py
@@ -25,6 +25,7 @@ _WS_GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
SESSIONS = {} # session id -> set of live WSHandler
LOCK = threading.Lock()
_SERVERS = []
+TOKENS = None # None = open relay; a set = hosts must present a valid access code
def local_ip():
@@ -86,12 +87,28 @@ class WSHandler(socketserver.BaseRequestHandler):
return
t = data.get('type')
if t == 'join':
- self.session_id = data.get('sessionId') or 'default'
+ sid = data.get('sessionId') or 'default'
+ if TOKENS is not None:
+ # hosts need an access code; patients may only join a LIVE session
+ # (they got the random room id from their therapist's link)
+ if data.get('role') == 'patient':
+ with LOCK:
+ live = bool(SESSIONS.get(sid))
+ if not live:
+ self._send_json({'type': 'join_denied', 'reason': 'session not live'})
+ return
+ elif data.get('token') not in TOKENS:
+ self._send_json({'type': 'join_denied', 'reason': 'access code required'})
+ return
+ self.session_id = sid
with LOCK:
SESSIONS.setdefault(self.session_id, set()).add(self)
self._send_json({'type': 'joined', 'sessionId': self.session_id})
elif t in ('state', 'patientViewport'):
- sid = data.get('sessionId') or self.session_id or 'default'
+ if TOKENS is not None and not self.session_id:
+ return # hardened relay only forwards for joined connections
+ sid = (self.session_id if TOKENS is not None
+ else data.get('sessionId') or self.session_id or 'default')
out = json.dumps({'type': t, 'payload': data.get('payload')})
with LOCK:
peers = list(SESSIONS.get(sid, ()))
@@ -219,9 +236,15 @@ def _argval(flag, default):
def main():
+ global TOKENS
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))
+ tokens_file = _argval('--tokens', None) # one access code per line, # comments ok
+ if tokens_file:
+ with open(tokens_file, encoding='utf-8') as f:
+ TOKENS = {l.strip() for l in f if l.strip() and not l.startswith('#')}
+ print('relay hardened: %d access codes loaded' % len(TOKENS))
socketserver.ThreadingTCPServer.allow_reuse_address = True
ThreadingHTTPServer.allow_reuse_address = True