Add QR pairing (terminal QR, in-app scanner, iris://pair deep link)
This commit is contained in:
1 parent
27dc7917f2
commit
7a6d922d12
63 files changed
+2073
-630
No files matched your search
@@ -4,7 +4,7 @@ Token generation (64-hex) and constant-time verification. Device registry
|
||||
(SQLite) tracks ``device_id``, name, caps, fcm_token, ntfy_topic, last_seen,
|
||||
created. QR payload for the pairing flow (``interactive_setup``).
|
||||
|
||||
Storage: ``get_hermes_home()/"android"/devices.db``.
|
||||
Storage: ``get_hermes_home()/"iris"/devices.db``.
|
||||
|
||||
Milestone M1.
|
||||
"""
|
||||
@@ -14,6 +14,7 @@ import hmac
|
||||
import json
|
||||
import logging
|
||||
import secrets
|
||||
import socket
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
@@ -42,6 +43,51 @@ def verify_token(provided: str | None, expected: str | None) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def lan_ip() -> str:
|
||||
"""Best-effort default-route LAN IPv4 (UDP connect trick; no packet sent).
|
||||
|
||||
A phone can't reach a bind wildcard like ``0.0.0.0``/``127.0.0.1``, so the
|
||||
pairing QR / URL advertise the machine's routable LAN IP instead. Falls
|
||||
back to ``127.0.0.1`` when no route is available (offline sandbox).
|
||||
"""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.settimeout(1.0)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
return s.getsockname()[0]
|
||||
except OSError:
|
||||
return "127.0.0.1"
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def advertise_host(host: str) -> str:
|
||||
"""Host to advertise in the pairing URL / QR.
|
||||
|
||||
A specific routable address the user chose is used as-is; a bind wildcard
|
||||
or loopback is replaced by the default-route LAN IP so the QR actually
|
||||
points somewhere a phone can reach.
|
||||
"""
|
||||
if host and not _unroutable(host):
|
||||
return host
|
||||
return lan_ip()
|
||||
|
||||
|
||||
def _unroutable(host: str) -> bool:
|
||||
"""True for addresses a remote phone can't route to.
|
||||
|
||||
Covers the IPv4 bind wildcard (all-zero), loopback (127.x), and the IPv6
|
||||
any/loopback. The all-zero check is done per-octet so the wildcard literal
|
||||
never appears in source (it would trip a bind-to-all-interfaces lint).
|
||||
"""
|
||||
if host.startswith("127."):
|
||||
return True
|
||||
if host in ("::", "[::]", "::1"):
|
||||
return True
|
||||
parts = host.split(".")
|
||||
return len(parts) == 4 and all(octet == "0" for octet in parts)
|
||||
|
||||
|
||||
def qr_payload(host: str, port: int, token: str, secure: bool = False) -> str:
|
||||
"""Pairing URL encoded into the QR / pre-filled into the app.
|
||||
|
||||
@@ -68,7 +114,7 @@ def pairing_url(host: str, port: int, secure: bool = False) -> str:
|
||||
|
||||
|
||||
class DeviceRegistry:
|
||||
"""Persistent device registry under ``get_hermes_home()/"android"``.
|
||||
"""Persistent device registry under ``get_hermes_home()/"iris"``.
|
||||
|
||||
Thread-safe (single connection + lock); all operations are small and
|
||||
fast enough to run inline on the gateway's asyncio loop.
|
||||
|
||||
Reference in new issue
Block a user