screen-locker/screen_locker/_http_workout_fetch.py
Krzysztof kuhy Rudnicki f3875302d7 Fix pylint findings in source and scripts
Real fixes: split over-long functions (lock-decision chain, manual
sync, validation, phone verification, status), hoist a dead lazy
import, context-manage Xvfb and temp dirs, add encodings, share the
silent-failure argv loop.

lock_grab_handoff read lock._recovery, which gatelock's LockWindow
does not have (its public property is recovery); a contract test now
checks every attribute the handoff reads against the real class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
2026-11-01 21:11:34 +01:00

95 lines
3.3 KiB
Python

"""HTTP fallback for phone verification: find the app's server, fetch JSON.
Split out of :mod:`screen_locker._phone_verification` to keep every file under
the 250-line cap. Composed back into ``PhoneVerificationMixin`` there, so
callers see no change.
Used when adb cannot reach the phone but it is on the same wifi: the workout
app serves the same JSON over ``WORKOUT_HTTP_PORT``.
"""
from __future__ import annotations
from concurrent.futures import (
ThreadPoolExecutor,
as_completed,
)
import contextlib
from http import client as _http_client
import json
import logging
import socket
from screen_locker._constants import WORKOUT_HTTP_PORT
_HTTPConnection = _http_client.HTTPConnection
_HTTPException = _http_client.HTTPException
_HTTP_OK = _http_client.OK
_logger = logging.getLogger(__name__)
class HttpWorkoutFetchMixin:
"""Scans the local subnet for the workout app's HTTP server."""
def _scan_for_http_server(self) -> str | None:
"""Scan local /24 subnet for the workout app HTTP server on port 8765.
Returns the first reachable URL or None.
"""
prefix = self._get_local_subnet_prefix()
if prefix is None:
return None
def probe(i: int) -> str | None:
ip = f"{prefix}.{i}"
with (
contextlib.suppress(OSError),
socket.create_connection((ip, WORKOUT_HTTP_PORT), timeout=0.3),
):
return f"http://{ip}:{WORKOUT_HTTP_PORT}/workout"
return None
_logger.info(
"Scanning %s.1-254:%d for workout app...", prefix, WORKOUT_HTTP_PORT
)
with ThreadPoolExecutor(max_workers=64) as executor:
for future in as_completed(
executor.submit(probe, i) for i in range(1, 255)
):
result = future.result()
if result is not None:
return result
return None
def _fetch_http_workout(self) -> dict | None:
"""Fetch workout JSON from the app's HTTP server on the local network.
Uses http.client directly to avoid urllib URL-open security lint rules.
The URL is always http://<local-ip>:8765/workout — no user input involved.
"""
url = self._scan_for_http_server()
if url is None:
return None
# url is always "http://<ip>:<port>/workout" — constructed internally.
try:
_, _, hostport = url.partition("://")
host, _, path = hostport.partition("/")
hostname, _, port_str = host.partition(":")
conn = _HTTPConnection(hostname, int(port_str), timeout=5)
conn.request("GET", f"/{path}")
resp = conn.getresponse()
if resp.status != _HTTP_OK:
return None
return json.loads(resp.read().decode())
except (_HTTPException, OSError, ValueError, json.JSONDecodeError) as exc:
_logger.warning(
"HTTP fetch of today's workout from %s failed (%s) — no data "
"from the phone's HTTP server, so verification falls through",
url,
exc,
)
return None
# ── Main verification entry point ─────────────────────────────────────────