tools: beacon fixes — cleartext, multi-device, validated-net POST, status URL

Debugging against a real restricted LAN surfaced three fixes:
- Android blocks app cleartext HTTP by default (targetSdk 36) — the port
  discovery + reachability were fine (phone curl reached fmr), only the
  app POST was denied. Added usesCleartextTraffic for this dev tool.
- Multi-device: report + receiver are keyed by device (Build.MODEL) so a
  phone and tablet don't clobber each other; connector connects each.
- POST over a VALIDATED internet network (prefer cellular) since the
  wireless-debug wifi is often a restricted LAN.
- Status/notification now show the target beacon URL + which network, per
  the request to surface what it's connecting to.
Verified live: beacon tracks the (frequently rotating) port via mDNS and
self-reports the current endpoint within seconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-07-31 22:07:35 +02:00
co-authored by Claude Opus 5
parent 52748ac853
commit 38d4440412
4 changed files with 75 additions and 25 deletions
+31 -20
View File
@@ -2,15 +2,15 @@
# SPDX-FileCopyrightText: 2026 Echolot contributors
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Tiny rendezvous receiver for the wireless-adb beacon. The phone POSTs its
# current wireless-debug endpoint here; the PC-side connector reads the stored
# file (over SSH) and runs `adb connect`. Dev tooling — not part of the product.
# Rendezvous receiver for the wireless-adb beacon (dev tool). Phones/tablets
# POST their current wireless-debug endpoint here, keyed by device; the PC-side
# connector reads them all and keeps `adb connect` current for each.
#
# POST /beacon { "ip": "...", "port": N } header: X-Beacon-Secret: <secret>
# GET /beacon -> the last stored JSON (no secret; it's just an ip:port)
# POST /beacon { "device": "...", "ip": "...", "port": N }
# header: X-Beacon-Secret: <secret>
# GET /beacon -> { "<device>": {ip, port, seen_at}, ... }
#
# Secret comes from ECHOLOT_BEACON_SECRET; stored state goes to STATE_PATH.
# Bind is 0.0.0.0:9099 so the phone can reach it over the internet/VPN.
# ECHOLOT_BEACON_SECRET gates POST; STATE_PATH holds the device map.
import json
import os
@@ -23,6 +23,22 @@ BIND = os.environ.get("BEACON_BIND", "0.0.0.0")
PORT = int(os.environ.get("BEACON_PORT", "9099"))
def load():
try:
with open(STATE_PATH) as f:
d = json.load(f)
return d if isinstance(d, dict) else {}
except Exception:
return {}
def store(d):
tmp = STATE_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump(d, f)
os.replace(tmp, STATE_PATH)
class Handler(BaseHTTPRequestHandler):
def _send(self, code, body=b"", ctype="application/json"):
self.send_response(code)
@@ -35,11 +51,7 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path.rstrip("/") != "/beacon":
return self._send(404, b'{"error":"not found"}')
try:
with open(STATE_PATH, "rb") as f:
self._send(200, f.read())
except FileNotFoundError:
self._send(200, b'{"available":false}')
self._send(200, json.dumps(load()).encode())
def do_POST(self):
if self.path.rstrip("/") != "/beacon":
@@ -49,19 +61,18 @@ class Handler(BaseHTTPRequestHandler):
n = int(self.headers.get("Content-Length", "0"))
try:
data = json.loads(self.rfile.read(n) or b"{}")
device = str(data.get("device", "default"))
ip = str(data["ip"])
port = int(data["port"])
except Exception:
return self._send(400, b'{"error":"need {ip, port}"}')
record = {"ip": ip, "port": port, "seen_at": int(time.time())}
tmp = STATE_PATH + ".tmp"
with open(tmp, "w") as f:
json.dump(record, f)
os.replace(tmp, STATE_PATH)
self._send(200, json.dumps(record).encode())
return self._send(400, b'{"error":"need {device, ip, port}"}')
d = load()
d[device] = {"ip": ip, "port": port, "seen_at": int(time.time())}
store(d)
self._send(200, json.dumps(d[device]).encode())
def log_message(self, *_):
pass # quiet
pass
if __name__ == "__main__":