"""LAN address discovery for the "scan to connect" QR flow. The QR encodes a deep link to the hosted PWA: http(s)://
/app#invite= so a phone's native camera app can scan it and open the login page with server URL + invite code already filled in. """ from __future__ import annotations import socket import urllib.parse def primary_lan_ip() -> str | None: """IP of the interface the default route goes through, without sending traffic.""" try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("10.255.255.255", 1)) # unroutable probe: no packets leave the host ip = s.getsockname()[0] finally: s.close() return None if ip.startswith("127.") else ip except OSError: return None def _lan_rank(ip: str) -> int: """Lower = more likely to be the address phones on the office LAN can reach.""" if ip.startswith("192.168."): return 0 if ip.startswith("10."): return 1 parts = ip.split(".") if parts[0] == "172" and parts[1].isdigit() and 16 <= int(parts[1]) <= 31: return 2 return 3 # CGNAT/VPN/odd ranges — last resort def lan_ip_candidates() -> list[str]: """All plausible LAN IPv4 addresses, best first, de-duplicated.""" ips: list[str] = [] primary = primary_lan_ip() if primary: ips.append(primary) try: for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET): ip = info[4][0] if not ip.startswith("127.") and ip not in ips: ips.append(ip) except OSError: pass return sorted(ips, key=_lan_rank) def host_port(host_header: str | None, scheme: str) -> tuple[str | None, int | None]: """Split the request Host header into (hostname, port). None when absent.""" if not host_header: return None, None try: split = urllib.parse.urlsplit("//" + host_header) port = split.port except ValueError: # malformed host header return None, None if port is None: port = 443 if scheme == "https" else None return (split.hostname or None), port def build_base_urls(request) -> list[str]: """Addresses phones in the team network can reach, best guess first. - Behind TLS (Caddy/https): the request's own Host is the public address phones should use, as-is. - Plain http (LAN mode): the browser's Host is often localhost, so swap in detected LAN IPs while keeping the request's port. The /app deep-link fragment (invite code) is appended by the QR endpoint. """ scheme = request.headers.get("x-forwarded-proto") or request.url.scheme host, port = host_port(request.headers.get("host"), scheme) if scheme == "https" and host: return [f"https://{host}/app"] urls = [] for ip in lan_ip_candidates(): urls.append(f"http://{ip}:{port}/app" if port else f"http://{ip}/app") if host and host not in ("localhost", "127.0.0.1") and not host.startswith("127."): urls.append(f"http://{host}:{port}/app" if port else f"http://{host}/app") return list(dict.fromkeys(urls)) # dedupe, keep order (host may equal a probed IP)