refractor

This commit is contained in:
2026-04-14 09:53:52 -04:00
parent 02d9e06077
commit e667af9201
13 changed files with 947 additions and 1629 deletions

View File

@@ -1,44 +0,0 @@
from typing import List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from .db_models import Base, Location, Route, Waypoint
DATABASE_URL = "sqlite+aiosqlite:///.locations.db"
engine = create_async_engine(DATABASE_URL, echo=True)
async_session_local = async_sessionmaker(engine, expire_on_commit=False)
async def init_db() -> None:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def create_location(name: str, address: str, latitude: float, longitude: float, is_favorite: bool = False):
async with async_session_local() as session:
new_location = Location(name=name, address=address, latitude=latitude, longitude=longitude, is_favorite=is_favorite)
session.add(new_location)
await session.commit()
return new_location
async def get_locations():
async with async_session_local() as session:
result = await session.execute(select(Location))
return result.scalars().all()
async def create_route(name: str, origin_id: int, destination_id: int, waypoints_data: List[dict]):
async with async_session_local() as session:
new_route = Route(name=name, origin_id=origin_id, destination_id=destination_id)
for wp in waypoints_data:
new_route.waypoints.append(Waypoint(order=wp['order'], description=wp['description']))
session.add(new_route)
await session.commit()
return new_route
async def get_routes():
async with async_session() as session:
# Use joinedload for efficient loading of relationships
from sqlalchemy.orm import joinedload
result = await session.execute(
select(Route).options(joinedload(Route.waypoints))
)
return result.scalars().unique().all()

View File

@@ -1,42 +0,0 @@
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.ext.asyncio import AsyncAttrs
from typing import List
class Base(AsyncAttrs, DeclarativeBase):
pass
class Location(Base):
__tablename__ = "locations"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
address: Mapped[str] = mapped_column(String(255))
latitude: Mapped[float]
longitude: Mapped[float]
is_favorite: Mapped[bool] = mapped_column(default=False)
routes: Mapped[List["Route"]] = relationship(back_populates="destination", cascade="all, delete-orphan")
class Route(Base):
__tablename__ = "routes"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
origin_id: Mapped[int] = mapped_column(ForeignKey("locations.id"))
destination_id: Mapped[int] = mapped_column(ForeignKey("locations.id"))
destination: Mapped["Location"] = relationship(back_populates="routes")
waypoints: Mapped[List["Waypoint"]] = relationship(back_populates="route", cascade="all, delete-orphan")
class Waypoint(Base):
__tablename__ = "waypoint"
id: Mapped[int] = mapped_column(primary_key=True)
order: Mapped[int]
description: Mapped[str]
route_id: Mapped[int] = mapped_column(ForeignKey("route.id"))
route: Mapped["Route"] = relationship(back_populates="waypoints")

View File

@@ -6,15 +6,21 @@ from geopy.adapters import AioHTTPAdapter
from geopy.extra.rate_limiter import AsyncRateLimiter
logger = logging.getLogger("ios-api")
CACHE_LOOKUP_SQL = "SELECT address FROM location_cache WHERE lat_lon = ?"
CACHE_UPSERT_SQL = "INSERT OR REPLACE INTO location_cache VALUES (?, ?)"
class AsyncReverseGeocoder:
def __init__(self, db_path="geocache.db", user_agent="pymd3_vue_location_sim/0.1.0 (iam@williambr.uno)"):
def __init__(
self,
db_path: str = "geocache.db",
user_agent: str = "pymd3_vue_location_sim/0.1.0 (iam@williambr.uno)",
):
self.db_path = db_path
self.user_agent = user_agent
self._init_db()
def _init_db(self):
def _init_db(self) -> None:
"""Initializes the SQLite database."""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
@@ -26,19 +32,29 @@ class AsyncReverseGeocoder:
''')
conn.commit()
async def get_address(self, lat, lon):
"""Reverse geocode with caching."""
key = f"{lat:.5f},{lon:.5f}"
logger.info("Checking location_cache for %s", key)
# Check Cache
with sqlite3.connect(self.db_path) as conn:
cursor = conn.execute(
"SELECT address FROM location_cache WHERE lat_lon = ?", (key,))
row = cursor.fetchone()
if row:
return json.loads(row[0])
@staticmethod
def _cache_key(lat: float, lon: float) -> str:
return f"{lat:.5f},{lon:.5f}"
def _read_cached_address(self, key: str) -> dict | None:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.execute(CACHE_LOOKUP_SQL, (key,))
row = cursor.fetchone()
return json.loads(row[0]) if row else None
def _store_cached_address(self, key: str, address_data: dict) -> None:
with sqlite3.connect(self.db_path) as conn:
conn.execute(CACHE_UPSERT_SQL, (key, json.dumps(address_data)))
conn.commit()
async def get_address(self, lat: float, lon: float) -> dict | None:
"""Reverse geocode with SQLite cache."""
key = self._cache_key(lat, lon)
logger.info("Checking location_cache for %s", key)
cached_address = self._read_cached_address(key)
if cached_address is not None:
return cached_address
# Fetch New Data
async with Nominatim(
user_agent=self.user_agent,
adapter_factory=AioHTTPAdapter
@@ -49,15 +65,10 @@ class AsyncReverseGeocoder:
location = await reverse(key)
if location:
logger.info("Nominatim response: %s", location)
address_data = location.raw['address']
# Save to Cache
conn.execute(
"INSERT OR REPLACE INTO location_cache VALUES (?, ?)",
(key, json.dumps(address_data))
)
conn.commit()
address_data = location.raw.get("address", {})
self._store_cached_address(key, address_data)
return address_data
return None
except Exception as e:
print(f"Error: {e}")
except Exception:
logger.exception("Reverse geocoding failed for key=%s", key)
return None

View File

@@ -2,6 +2,7 @@ import asyncio
import logging
import os
import sys
import textwrap
from collections.abc import Callable
import click
@@ -16,10 +17,17 @@ from pyicloud.exceptions import (
PyiCloudPasswordException,
)
from .models import iCloudReturnData
from .models import ICloudReturnData
load_dotenv()
logger = logging.getLogger("ios-api")
COOKIE_DIRECTORY = "./cookies"
ENV_APPLE_ID = "APPLE_ID"
ENV_APPLE_PW = "APPLE_PW"
ENV_SELECTED_DEVICE_ID = "SELECTED_DEVICE_ID"
ENV_SELECTED_DEVICE_NAME = "SELECTED_DEVICE_NAME"
ENV_AUTH_INIT_TIMEOUT = "ICLOUD_AUTH_INIT_TIMEOUT_SECONDS"
BACKOFF_SCHEDULE = (15, 30, 60, 120, 300)
AUTH_EXCEPTIONS = (
PyiCloudAuthRequiredException,
PyiCloudFailedLoginException,
@@ -38,11 +46,11 @@ class FindMyMonitor:
get_client_sids: Callable[[], list[str]] | None = None,
code_timeout_seconds: int = 180,
):
self.username = os.getenv("APPLE_ID")
self.password = os.getenv("APPLE_PW")
self.username = os.getenv(ENV_APPLE_ID)
self.password = os.getenv(ENV_APPLE_PW)
self.token_file = token_file
self.selected_device_id = os.getenv("SELECTED_DEVICE_ID")
self.selected_device_name = os.getenv("SELECTED_DEVICE_NAME")
self.selected_device_id = os.getenv(ENV_SELECTED_DEVICE_ID)
self.selected_device_name = os.getenv(ENV_SELECTED_DEVICE_NAME)
self.selected_device = None
self.queue = queue
self.api = None
@@ -52,13 +60,33 @@ class FindMyMonitor:
self.get_client_sids = get_client_sids
self.code_timeout_seconds = code_timeout_seconds
self.auth_init_timeout_seconds = int(
os.getenv("ICLOUD_AUTH_INIT_TIMEOUT_SECONDS", "0")
os.getenv(ENV_AUTH_INIT_TIMEOUT, "0")
)
self._logged_candidates = False
self._no_location_streak = 0
self._auth_error_streak = 0
self._fetch_lock = asyncio.Lock()
async def _create_api_session(self, has_token: bool) -> bool:
try:
init_args = [self.username]
if not has_token:
init_args.append(self.password)
init_task = asyncio.to_thread(
PyiCloudService, *init_args, cookie_directory=COOKIE_DIRECTORY
)
if self.auth_init_timeout_seconds > 0:
self.api = await asyncio.wait_for(
init_task, timeout=self.auth_init_timeout_seconds
)
else:
self.api = await init_task
return True
except Exception:
source = "cookies" if has_token else "credentials"
logger.exception("Failed to initialize iCloud session from %s", source)
return False
async def _request_code_from_vue(self, prompt: str) -> str | None:
if self.sio is None or self.get_client_sids is None:
logger.warning("2FA request skipped: Socket.IO context not configured")
@@ -91,7 +119,7 @@ class FindMyMonitor:
logger.warning("Invalid 2FA code payload from sid=%s", sid)
return None
async def authenticate(self):
async def authenticate(self) -> bool:
"""Authenticates with iCloud, handling 2FA and token storage."""
if not self.username:
logger.warning("APPLE_ID is not configured; skipping iCloud monitor authentication")
@@ -103,42 +131,15 @@ class FindMyMonitor:
)
return False
if os.path.exists(self.token_file):
print("Loading stored session...")
try:
init_task = asyncio.to_thread(
PyiCloudService, self.username, cookie_directory="./cookies"
)
if self.auth_init_timeout_seconds > 0:
self.api = await asyncio.wait_for(
init_task, timeout=self.auth_init_timeout_seconds
)
else:
self.api = await init_task
except Exception as e:
logger.exception("Failed to initialize iCloud session from cookies: %s", e)
return False
else:
print("No stored session. Authenticating...")
try:
init_task = asyncio.to_thread(
PyiCloudService,
self.username,
self.password,
cookie_directory="./cookies",
)
if self.auth_init_timeout_seconds > 0:
self.api = await asyncio.wait_for(
init_task, timeout=self.auth_init_timeout_seconds
)
else:
self.api = await init_task
except Exception as e:
logger.exception("Failed to initialize iCloud session with credentials: %s", e)
return False
logger.info(
"Initializing iCloud session from %s",
"stored cookies" if has_token else "credentials",
)
if not await self._create_api_session(has_token=has_token):
return False
if self.api.requires_2fa:
print("Two-factor authentication required.")
logger.info("Two-factor authentication required.")
code = await self._request_code_from_vue("Enter the 6-digit Apple verification code")
if code is None:
if sys.stdin and sys.stdin.isatty():
@@ -154,29 +155,28 @@ class FindMyMonitor:
"2FA required but no interactive terminal or Vue responder is available; deferring authentication"
)
return False
# Verify the code
result = await asyncio.to_thread(self.api.validate_2fa_code, code)
print(f"2FA validation result: {result}")
logger.info("2FA validation result: %s", result)
if not result:
print("Failed to verify 2FA code")
logger.warning("Failed to verify 2FA code")
return False
# Trust the session
await asyncio.to_thread(self.api.trust_session)
if self.api.requires_2sa:
import textwrap
print(textwrap.dedent("""
logger.info(textwrap.dedent("""
Two-step authentication required.
Please select a device to receive a SMS verification code:
"""))
# List available devices for 2SA
for i, device in enumerate(self.api.trusted_devices):
print(
f" {i + 1}: {device.get('deviceName', 'Unknown device')} ({device.get('phoneNumber', 'Unknown number')})")
logger.info(
" %s: %s (%s)",
i + 1,
device.get("deviceName", "Unknown device"),
device.get("phoneNumber", "Unknown number"),
)
# Prompt the user for their choice
if not (sys.stdin and sys.stdin.isatty()):
logger.warning(
"2SA required but no interactive terminal is available; deferring authentication"
@@ -185,19 +185,18 @@ class FindMyMonitor:
device_index = await asyncio.to_thread(click.prompt, "Please select a device number", type=int) - 1
device = self.api.trusted_devices[device_index]
if not await asyncio.to_thread(self.api.send_verification_code, device):
print("Failed to send verification code")
logger.warning("Failed to send verification code")
return False
# Prompt the user to enter the verification code they received
code = await asyncio.to_thread(click.prompt, "Please enter verification code", type=int)
if not await asyncio.to_thread(self.api.validate_verification_code, device, code):
print("Failed to verify verification code")
logger.warning("Failed to verify verification code")
return False
print("Successfully authenticated.")
logger.info("Successfully authenticated.")
return True
async def get_location(self):
async def get_location(self) -> ICloudReturnData | None:
"""Fetches the latest latitude and longitude."""
if not self.api:
await self.authenticate()
@@ -259,7 +258,7 @@ class FindMyMonitor:
"deviceStatus": status['deviceStatus'],
"name": status['name']
}
response = iCloudReturnData(**data)
response = ICloudReturnData(**data)
return response
logger.info("Location payload is None for device=%s", self.selected_device.name)
return None
@@ -279,14 +278,12 @@ class FindMyMonitor:
self.running = False
def _no_location_backoff_seconds(self, base_interval: int) -> int:
schedule = [15, 30, 60, 120, 300]
idx = min(self._no_location_streak - 1, len(schedule) - 1)
return max(base_interval, schedule[idx])
idx = min(self._no_location_streak - 1, len(BACKOFF_SCHEDULE) - 1)
return max(base_interval, BACKOFF_SCHEDULE[idx])
def _auth_backoff_seconds(self, base_interval: int) -> int:
schedule = [15, 30, 60, 120, 300]
idx = min(self._auth_error_streak - 1, len(schedule) - 1)
return max(base_interval, schedule[idx])
idx = min(self._auth_error_streak - 1, len(BACKOFF_SCHEDULE) - 1)
return max(base_interval, BACKOFF_SCHEDULE[idx])
async def refresh_location(self):
"""Fetch one location update while serializing iCloud API access."""
@@ -308,7 +305,12 @@ class FindMyMonitor:
if device_data is not None:
self._no_location_streak = 0
self._auth_error_streak = 0
print(f"{device_data.timeStamp} - Location: {device_data.latitude}, {device_data.longitude}")
logger.info(
"%s - Location: %s, %s",
device_data.timeStamp,
device_data.latitude,
device_data.longitude,
)
await self.queue.put(device_data)
else:
self._no_location_streak += 1

View File

@@ -1,213 +0,0 @@
from pymobiledevice3.services.dvt.instruments.location_simulation_base import (
LocationSimulationBase,
)
from pymobiledevice3.services.dvt.instruments.location_simulation import (
LocationSimulation,
)
class LocationSimulationQueue(LocationSimulation):
def __init__(self, dvt, context: LocationSimulationState):
super().__init__(dvt)
self.context = context
async def play_queue(
self, disable_sleep: bool = False, timing_randomness_range: int = 0
) -> None:
while True:
if self.context.queue_state == "PAUSED":
await asyncio.sleep(0.1)
continue
if self.context.queue_state == "SHUTDOWN":
break
loc_id = await self.context.queue.get()
if loc_id is None:
self.context.queue.task_done()
break
location_item = self.context.queue_data.get(loc_id)
if location_item is None:
logger.warning(
"Simulation queue item missing for loc_id=%s; skipping stale entry",
loc_id,
)
self.context.queue.task_done()
continue
new_latitude = location_item.get("latitude")
new_longitude = location_item.get("longitude")
new_delay = location_item.get("delay")
new_delay = 0 if new_delay is None else new_delay
new_start = location_item.get("start")
current_location_item = self.context.queue_data.get(self.context.loc_id)
current_latitude = (
current_location_item.get("latitude")
if isinstance(current_location_item, dict)
else self.context.latitude
)
current_longitude = (
current_location_item.get("longitude")
if isinstance(current_location_item, dict)
else self.context.longitude
)
current_start = (
current_location_item.get("start")
if isinstance(current_location_item, dict)
else None
)
if self.context.set_location_enabled:
if new_delay > 0 and not disable_sleep:
countdown_delay = int(round(float(new_delay)))
if timing_randomness_range > 0:
new_delay = new_delay + random.uniform(
-timing_randomness_range, timing_randomness_range
)
countdown_delay = int(round(float(new_delay)))
for i in range(max(0, countdown_delay), 0, -1):
self.context.next_move = i
await self.context.sio.emit(
"simulation_status",
{
"status": self.context.simulation_active,
"loc_id": self.context.loc_id,
"latitude": current_latitude,
"longitude": current_longitude,
"start": current_start,
"next_move": i,
},
namespace="/",
)
await asyncio.sleep(1)
self.context.queue_data[loc_id]["start"] = self.context.queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat()
await self.set(new_latitude, new_longitude)
self.context.loc_id = loc_id
self.context.latitude = new_latitude
self.context.longitude = new_longitude
await self.context.sio.emit(
"simulation_status",
{
"status": self.context.simulation_active,
"loc_id": self.context.loc_id,
"latitude": self.context.latitude,
"longitude": self.context.longitude,
"start": new_start,
"next_move": None,
},
namespace="/",
)
logger.info(
"Set simulated location to %s, %s after %ss delay",
new_latitude,
new_longitude,
new_delay,
)
self.context.queue.task_done()
class LocationSimulationTestQueue(LocationSimulationBase):
def __init__(self, context: LocationSimulationState):
super().__init__()
self.context = context
def __enter__(self):
return self
def __exit__(self):
return self
async def set(self, latitude: float, longitude: float) -> None:
await asyncio.sleep(0.1)
logger.info("Simulated location set to %s, %s", latitude, longitude)
async def clear(self) -> None:
q = self.context.queue
self.context.set_location_enabled = False
self.context.queue_state = "SHUTDOWN"
while not q.empty():
try:
item = q.get_nowait()
q.task_done()
logger.info("Discarding item from queue: %s", item)
except asyncio.QueueEmpty:
break
await q.put(None)
if self.context.simulation_task is not None and not self.context.simulation_task.done():
try:
await asyncio.wait_for(self.context.simulation_task, timeout=5)
except TimeoutError:
self.context.simulation_task.cancel()
with suppress(asyncio.CancelledError):
await self.context.simulation_task
self.context.simulation_active = False
self.context.queue_state = "SHUTDOWN"
async def play_queue(
self, disable_sleep: bool = False, timing_randomness_range: int = 0
) -> None:
while True:
if self.context.queue_state == "PAUSED":
await asyncio.sleep(0.1)
continue
if self.context.queue_state == "SHUTDOWN":
break
loc_id = await self.context.queue.get()
if loc_id is None:
self.context.queue.task_done()
break
location_item = self.context.queue_data.get(loc_id)
if location_item is None:
logger.warning(
"Test simulation queue item missing for loc_id=%s; skipping stale entry",
loc_id,
)
self.context.queue.task_done()
continue
latitude = location_item.get("latitude")
longitude = location_item.get("longitude")
delay = location_item.get("delay")
delay = 0 if delay is None else delay
start_time = location_item.get("start")
if self.context.set_location_enabled:
if delay > 0 and not disable_sleep:
countdown_delay = int(round(float(delay)))
if timing_randomness_range > 0:
delay = delay + random.uniform(
-timing_randomness_range, timing_randomness_range
)
countdown_delay = int(round(float(delay)))
for i in range(max(0, countdown_delay), 0, -1):
self.context.next_move = i
await self.context.sio.emit(
"simulation_status",
{
"status": self.context.simulation_active,
"loc_id": self.context.loc_id,
"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_move": i,
},
namespace="/",
)
await asyncio.sleep(1)
await self.set(latitude, longitude)
self.context.latitude = latitude
self.context.longitude = longitude
await self.context.sio.emit(
"simulation_status",
{
"status": self.context.simulation_active,
"loc_id": self.context.loc_id,
"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_move": None,
},
namespace="/",
)
logger.info(
"Set simulated location to %s, %s after %ss delay",
latitude,
longitude,
delay,
)
self.context.queue.task_done()

View File

@@ -1,59 +1,63 @@
from typing import Optional, Dict
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel
class SimulationStatusData(BaseModel):
class Coordinate(BaseModel):
latitude: float
longitude: float
class ScheduledCoordinate(Coordinate):
delay: int = 0
start: Optional[str] = None
end: Optional[str] = None
class SimulationStatusData(Coordinate):
start: float
end: Optional[float]
next_move: Optional[float]
end: Optional[float] = None
next_move: Optional[float] = None
class SimulationStatus(BaseModel):
status: bool
data: Optional[SimulationStatusData]
data: Optional[SimulationStatusData] = None
class SimulationRequestData(BaseModel):
latitude: float
longitude: float
delay: int = 0
start: Optional[str] = None
end: Optional[str] = None
class SimulationRequestData(ScheduledCoordinate):
pass
class SimulationRequest(BaseModel):
status: bool
data: Optional[SimulationRequestData]
data: Optional[SimulationRequestData] = None
class SimulationRequestResponseData(BaseModel):
class SimulationRequestResponseData(ScheduledCoordinate):
loc_id: str
latitude: float
longitude: float
delay: int = 0
start: Optional[str] = None
end: Optional[str] = None
class SimulationQueueList(BaseModel):
data: Optional[SimulationRequestResponseData]
data: Optional[SimulationRequestResponseData] = None
class SimulationRequestResponse(BaseModel):
status: bool
data: Optional[SimulationRequestResponseData]
data: Optional[SimulationRequestResponseData] = None
class SimulationQueueDict(BaseModel):
location_id: Dict[str, SimulationRequestResponseData]
location_id: dict[str, SimulationRequestResponseData]
class iCloudLocationData(BaseModel):
latitude: float
longitude: float
class ICloudLocationData(Coordinate):
timestamp: str
class iCloudReturnData(BaseModel):
latitude: float
longitude: float
class ICloudReturnData(Coordinate):
timeStamp: int
altitude: float
horizontalAccuracy: float
@@ -63,11 +67,16 @@ class iCloudReturnData(BaseModel):
deviceStatus: int
name: str
class LatLng(BaseModel):
latitude: float
longitude: float
class LatLng(Coordinate):
pass
class ORSRequest(BaseModel):
geometry_simplify: bool
coordinates: List[List[float]]
coordinates: list[list[float]]
# Backward compatibility aliases for existing imports.
iCloudLocationData = ICloudLocationData
iCloudReturnData = ICloudReturnData

View File

@@ -1,379 +0,0 @@
""" Tunnel Functions"""
@self._app.get("/start-tunnel")
async def start_tunnel(
udid: Optional[str] = self.context.udid,
ip: Optional[str] = None,
connection_type: Optional[str] = None,
) -> fastapi.Response:
udid_tunnels = [
t.tunnel
for t in self._tunneld_core.tunnel_tasks.values()
if t.udid == udid and t.tunnel is not None
]
if len(udid_tunnels) > 0:
self.context.udid = udid
data = {
"interface": udid_tunnels[0].interface,
"port": udid_tunnels[0].port,
"address": udid_tunnels[0].address,
}
return generate_http_response(data)
queue = asyncio.Queue()
created_task = False
try:
if not created_task and connection_type in ("usbmux", None):
task_identifier = f"usbmux-{udid}"
try:
async with await create_using_usbmux(udid) as lockdown:
service = await CoreDeviceTunnelProxy.create(lockdown)
task = asyncio.create_task(
self._tunneld_core.start_tunnel_task(
task_identifier,
service,
protocol=TunnelProtocol.TCP,
queue=queue,
),
name=f"start-tunnel-task-{task_identifier}",
)
self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(
task=task, udid=udid
)
created_task = True
except ConnectionFailedError, InvalidServiceError, MuxException:
pass
if connection_type in ("usb", None):
for rsd in await get_rsds(udid=udid):
rsd_ip = rsd.service.address[0]
if ip is not None and rsd_ip != ip:
await rsd.close()
continue
task = asyncio.create_task(
self._tunneld_core.start_tunnel_task(
rsd_ip,
await create_core_device_tunnel_service_using_rsd(rsd),
queue=queue,
),
name=f"start-tunnel-usb-{rsd_ip}",
)
self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(
task=task, udid=rsd.udid
)
created_task = True
if not created_task and connection_type in ("wifi", None):
for remotepairing in await get_remote_pairing_tunnel_services(
udid=udid
):
remotepairing_ip = remotepairing.hostname
if ip is not None and remotepairing_ip != ip:
await remotepairing.close()
continue
task = asyncio.create_task(
self._tunneld_core.start_tunnel_task(
remotepairing_ip, remotepairing, queue=queue
),
name=f"start-tunnel-wifi-{remotepairing_ip}",
)
self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
task=task, udid=remotepairing.remote_identifier
)
created_task = True
except Exception as e:
return fastapi.Response(
status_code=501,
content=json.dumps(
{
"error": {
"exception": e.__class__.__name__,
"traceback": traceback.format_exc(),
}
}
),
)
if not created_task:
return fastapi.Response(
status_code=501, content=json.dumps({"error": "task not created"})
)
tunnel: Optional[TunnelResult] = await queue.get()
if tunnel is not None:
self.context.udid = udid
data = {
"interface": tunnel.interface,
"port": tunnel.port,
"address": tunnel.address,
}
return generate_http_response(data)
else:
return fastapi.Response(
status_code=404,
content=json.dumps(
{"error": "something went wrong during tunnel creation"}
),
)
@self._app.get("/restart-tunneld")
async def restart() -> fastapi.Response:
"""Restart Tunneld"""
self._tunneld_core.clear()
await asyncio.sleep(2)
self._tunneld_core.start()
data = {
"operation": "restart-tunneld",
"data": True,
"message": "Restarting tunneld...",
}
return generate_http_response(data)
@self._app.get("/shutdown")
async def shutdown() -> fastapi.Response:
"""Shutdown Tunneld"""
os.kill(os.getpid(), signal.SIGINT)
data = {
"operation": "shutdown",
"data": True,
"message": "Server shutting down...",
}
return generate_http_response(data)
@self._app.get("/clear-tunnels")
async def clear_tunnels() -> fastapi.Response:
"""Clear all tunnels"""
self._tunneld_core.clear()
data = {
"operation": "clear_tunnels",
"data": True,
"message": "Cleared tunnels...",
}
return generate_http_response(data)
@self._app.get("/cancel")
async def cancel_tunnel(udid: str) -> fastapi.Response:
"""Cancel a tunnel"""
self._tunneld_core.cancel(udid=udid)
data = {
"operation": "cancel",
"udid": udid,
"data": True,
"message": f"tunnel {udid} Canceled ...",
}
return generate_http_response(data)
"""Simulation Functions"""
@self._app.get("/start-simulation")
async def app_start_simulation() -> fastapi.Response:
logger.info("Simulation Start Requested ")
if (
self.context.simulation_task is None
or self.context.simulation_task.done()
):
await start_icloud_monitor()
self.context.simulation_active = True
self.context.simulation_task = asyncio.create_task(
start_simulation_queue(),
name="location-simulation-worker",
)
data = {"status": "started", "message": "Simulation started"}
else:
data = {"status": "error", "message": "Simulation already running"}
return generate_http_response(data)
@self._app.get("/start-icloud-monitor")
async def app_start_icloud_monitor() -> fastapi.Response:
await start_icloud_monitor()
data = {
"status": "started",
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
return generate_http_response(data)
@self._app.get("/stop-icloud-monitor")
async def app_stop_icloud_monitor() -> fastapi.Response:
await end_icloud_monitor()
data = {
"status": "stopped",
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
return generate_http_response(data)
@self._app.get("/icloud-monitor-status")
async def app_icloud_monitor_status() -> fastapi.Response:
data = {
"status": "ok",
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
return generate_http_response(data)
@self._app.post("/add-location")
async def app_add_location(data: SimulationRequestData) -> fastapi.Response:
"""Add a location to the simulation queue"""
logger.info("Request to add new location to queue")
loc_id = str(uuid.uuid4())
latitude = (
data.get("latitude")
if isinstance(data, dict)
else getattr(data, "latitude", None)
)
longitude = (
data.get("longitude")
if isinstance(data, dict)
else getattr(data, "longitude", None)
)
delay = (
data.get("delay", 0)
if isinstance(data, dict)
else getattr(data, "delay", 0)
)
try:
delay = parse_delay_seconds(delay)
except ValueError as e:
return generate_http_response(
{"status": "error", "message": str(e)},
status_code=400,
)
if latitude is not None and longitude is not None:
logger.info(
"Adding location %s (%s, %s) with %s delay to the queue",
loc_id,
latitude,
longitude,
delay,
)
accrued_delay = 0
if self.context.queue_data:
accrued_delay = sum(
parse_delay_seconds(item.get("delay", 0))
for item in self.context.queue_data.values()
)
now_time = datetime.now(timezone.utc)
new_time = (
now_time
+ timedelta(seconds=accrued_delay)
+ timedelta(seconds=delay)
)
start_time = new_time.isoformat()
location_item = {
"loc_id": loc_id,
"latitude": latitude,
"longitude": longitude,
"delay": delay,
"start": start_time,
"status": "queued",
}
resp = {
"status": "added",
"message": f"Location {loc_id} added to the queue",
"item": location_item,
}
await self.context.queue.put(loc_id)
add_item(loc_id, location_item)
logger.info("Location %s added to the queue", loc_id)
else:
resp = {"status": "error", "message": "Invalid location data"}
return generate_http_response(resp)
@self._app.get("/clear-queue")
async def app_clear_queue() -> fastapi.Response:
"""Clear the simulation queue"""
logger.info("Simulation Start Requested ")
await empty_simulation_queue()
data = {"status": "cleared", "message": "Simulation cleared"}
return generate_http_response(data)
@self._app.get("/pause-queue")
async def app_pause_queue() -> fastapi.Response:
"""Pause the simulation queue"""
await pause_simulation_queue()
data = {"status": "paused", "message": "Simulation paused"}
return generate_http_response(data)
@self._app.get("/resume-queue")
async def app_resume_queue() -> fastapi.Response:
"""Resume the simulation queue"""
await resume_simulation_queue()
data = {"status": "resumed", "message": "Simulation resumed"}
return generate_http_response(data)
@self._app.get("/end-simulation")
async def app_end_simulation() -> fastapi.Response:
"""End the simulation queue"""
logger.info("End location simulation request")
end_task = asyncio.create_task(
end_simulation_queue(), name="end-simulation-worker"
)
result = await end_task
data = {"status": result, "message": "Simulation ended"}
return generate_http_response(data)
"""Status Functions"""
@self._app.get("/")
async def list_tunnels() -> dict[str, list[dict]]:
"""Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}"""
tunnels = {}
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
continue
if active_tunnel.udid not in tunnels:
tunnels[active_tunnel.udid] = []
tunnels[active_tunnel.udid].append(
{
"tunnel-address": active_tunnel.tunnel.address,
"tunnel-port": active_tunnel.tunnel.port,
"interface": ip,
}
)
return tunnels
@self._app.get("/device-info")
async def device_info():
"""Get device information"""
tunnels = {}
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
continue
if active_tunnel.udid not in tunnels:
tunnels[active_tunnel.udid] = {}
try:
lockdown = await create_using_usbmux(
serial=active_tunnel.udid, autopair=False
)
tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values)
except Exception as e:
logger.error(
f"Failed to create lockdown session for device {active_tunnel.udid}: {e}"
)
continue
return tunnels
@self._app.get("/device-name")
async def rsd_info():
"""Get rsd information"""
device_name = await get_device_name()
return generate_http_response(device_name)
@self._app.get("/rsd-info")
async def rsd_info():
"""Get rsd information"""
rsd_info = {}
if self.context.tunnel is None:
await get_tun()
if self.context.tunnel is not None:
rsd_info = self.context.tunnel.peer_info
return generate_http_response(rsd_info)
@self._app.get("/hello")
async def hello() -> fastapi.Response:
data = {"message": "Hello, I'm alive"}
return generate_http_response(data)
@self._app.get("/context-status")
async def app_context_status() -> fastapi.Response:
data = get_status()
return generate_http_response(data)

View File

@@ -1,425 +0,0 @@
""" Socket.IO Functions"""
async def sio_send_status(sid):
"""Send Current Status"""
await self.context.sio.emit(
"status", jsonable_encoder(get_status()), namespace="/", to=sid
)
"""Socket.IO Connection Events"""
@self.context.sio.event
async def connect(sid, environ):
"""Client connection event handler."""
self.context.connected_clients.add(sid)
logger.info("Client connected: %s", sid)
await sio_send_status(sid)
return "%s connected" % sid
@self.context.sio.event
async def disconnect(sid):
"""Client disconnection event handler."""
self.context.connected_clients.discard(sid)
logger.info("Client disconnected: %s", sid)
""" Socket.IO Request Events """
@self.context.sio.event
async def request_update(sid):
status_update = jsonable_encoder(get_status())
logger.info("Update request from %s sending %s", sid, status_update)
return status_update
@self.context.sio.event
async def message(sid, data):
logger.info("Received message from %s: %s", sid, data)
return True, "Message received"
# await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/")
""" Device Control"""
@self.context.sio.event
async def device_control(sid, data):
"""Device Control"""
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
delay = (
data.get("delay")
if isinstance(data, dict)
else getattr(data, "delay", None)
)
if delay is None:
delay = 5
match command:
case "shutdown":
""" Shutdown the device"""
logger.info(
"Shutdown command received from %s with delay %s", sid, delay
)
await device_shutdown(delay)
return {
"command": "shutdown",
"status": "success",
"message": f"Device shutdown initiated with {delay} seconds delay",
}
case "reboot":
""" Reboot the device"""
logger.info(
"Reboot command received from %s with delay %s", sid, delay
)
await device_reboot(delay)
return {
"command": "reboot",
"status": "success",
"message": f"Device reboot initiated with {delay} seconds delay",
}
case _:
return {
"command": command,
"status": "error",
"message": f"Invalid command: {command}",
}
@self.context.sio.event
async def simulation_control(sid, data):
"""Simulation Control"""
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info(
"Simulation Control command: %s requested from %s", command, sid
)
try:
match command:
case "add":
""" Add a location to the simulation queue"""
loc_id = str(uuid.uuid4())
latitude = (
data.get("latitude")
if isinstance(data, dict)
else getattr(data, "latitude", None)
)
longitude = (
data.get("longitude")
if isinstance(data, dict)
else getattr(data, "longitude", None)
)
delay = (
data.get("delay", 0)
if isinstance(data, dict)
else getattr(data, "delay", 0)
)
try:
delay = parse_delay_seconds(delay)
except ValueError as e:
return {
"command": command,
"status": "error",
"message": str(e),
}
if latitude is not None and longitude is not None:
logger.info(
"Adding location %s (%s, %s) with %s delay to the queue",
loc_id,
latitude,
longitude,
delay,
)
accrued_delay = 0
if self.context.queue_data:
accrued_delay = sum(
parse_delay_seconds(item.get("delay", 0))
for item in self.context.queue_data.values()
)
now_time = datetime.now(timezone.utc)
new_time = (
now_time
+ timedelta(seconds=accrued_delay)
+ timedelta(seconds=delay)
)
start_time = new_time.isoformat()
coords = f"{latitude}, {longitude}"
rev_geocode = self.context.reverse_geocode(coords)
if rev_geocode:
address = rev_geocode.address
else:
address = f"{latitude}, {longitude}"
location_item = {
"loc_id": loc_id,
"latitude": latitude,
"longitude": longitude,
"delay": delay,
"start": start_time,
"address": address,
}
ack = {
"command": command,
"status": "added",
"message": f"Location {loc_id} added to the queue",
"item": location_item,
}
await self.context.queue.put(loc_id)
add_item(loc_id, location_item)
logger.info("Location %s added to the queue", loc_id)
return ack
else:
logger.warning(
"Invalid location data received from %s: %s", sid, data
)
return {
"command": command,
"status": "error",
"message": "Invalid location data",
"data": location_item,
}
case "clear":
""" Clear the simulation queue"""
await empty_simulation_queue()
return {
"command": command,
"status": "cleared",
"message": "Simulation cleared",
}
case "pause":
""" Pause the simulation queue"""
await pause_simulation_queue()
return {
"command": command,
"status": "paused",
"message": "Simulation paused",
}
case "resume":
""" Resume the simulation queue"""
await resume_simulation_queue()
return {
"command": command,
"status": "resumed",
"message": "Simulation resumed",
}
case "end":
""" End the simulation queue"""
logger.info("End location simulation request from %s", sid)
end_task = asyncio.create_task(
end_simulation_queue(), name="end-simulation-worker"
)
result = await end_task
simstatus = not result
return {
"command": command,
"status": simstatus,
"message": "Simulation ended",
}
case "start":
""" Start the simulation queue"""
logger.info("Start location simulation request from %s", sid)
if (
self.context.simulation_task is None
or self.context.simulation_task.done()
):
await start_icloud_monitor()
self.context.simulation_active = True
self.context.queue_state = "RUNNING"
self.context.simulation_task = asyncio.create_task(
start_simulation_queue(),
name="location-simulation-worker",
)
return {
"command": command,
"status": self.context.queue_state,
"message": "Simulation started",
}
else:
return {
"command": command,
"status": "error",
"message": "Simulation already running",
}
case _:
logger.warning(
"Invalid command received from %s: %s", sid, command
)
return {"status": "error", "message": "Invalid command"}
finally:
await sio_send_status(sid)
@self.context.sio.event
async def icloud_monitor_control(sid, data):
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info(
"iCloud Monitor control command: %s requested from %s", command, sid
)
try:
match command:
case "start":
await start_icloud_monitor()
return {
"command": command,
"status": "running",
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
case "stop":
await end_icloud_monitor()
return {
"command": command,
"status": "stopped",
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
case "status":
return {
"command": command,
"status": "ok",
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
case _:
return {
"command": command,
"status": "error",
"message": "Invalid command",
}
finally:
await sio_send_status(sid)
""" Tunnel Control """
@self.context.sio.event
async def tunneld_control(sid, data):
command = (
data.get("command")
if isinstance(data, dict)
else getattr(data, "command", None)
)
logger.info("Tunneld Control command: %s requested from %s", command, sid)
match command:
case "start":
"""Start Tunneld"""
logger.info("Start tunneld request from %s: %s", sid, data)
try:
self._tunneld_core.start()
logger.info("Tunneld started successfully")
return {
"status": "running",
"message": "Tunneld started successfully",
}
except Exception as e:
logger.error("Error starting tunneld: %s", e)
return {
"command": command,
"status": "error",
"message": f"Error starting tunneld: {e}",
}
case "start-watcher":
""" Start Tunneld Watcher """
logger.info("Start tunneld watcher request from %s: %s", sid, data)
await start_tunnel_watcher()
return {
"status": "running",
"message": "Tunneld watcher started successfully",
}
case "end-watcher":
""" End Tunneld Watcher """
logger.info("End tunneld watcher request from %s: %s", sid, data)
await end_tunnel_watcher()
return {
"status": "stopped",
"message": "Tunneld watcher stopped successfully",
}
case "shutdown":
"""Shutdown Tunneld"""
logger.info("Shutdown tunneld request from %s: %s", sid, data)
try:
os.kill(os.getpid(), signal.SIGINT)
return {
"command": command,
"status": "Success",
"message": "Server shutting down...",
}
except Exception as e:
logger.error("Error shutting down tunneld: %s", e)
return {
"command": command,
"status": "error",
"message": f"Error shutting down tunneld: {e}",
}
case "clear":
"""Clear all tunnels"""
logger.info("Clearing tunnels...")
try:
self._tunneld_core.clear()
return {
"command": command,
"status": "Success",
"message": "Cleared tunnels...",
}
except Exception as e:
logger.error("Error clearing tunnels: %s", e)
return {
"command": command,
"status": "error",
"message": f"Error clearing tunnels: {e}",
}
case "cancel":
"""Cancel a tunnel"""
logger.info("Canceling tunnel request from %s: %s", sid, data)
try:
udid = (
data.get("udid")
if isinstance(data, dict)
else getattr(data, "udid", self.context.udid)
)
if udid is None:
udid = self.context.udid
self._tunneld_core.cancel(udid=udid)
return {
"command": command,
"status": "Success",
"udid": udid,
"message": f"tunnel {udid} Canceled ...",
}
except Exception as e:
logger.error("Error canceling tunnel: %s", e)
return {
"command": command,
"status": "error",
"message": f"Error canceling tunnel: {e}",
}
case _:
return {
"command": command,
"status": "error",
"message": f"Unknown operation: {command}",
}

File diff suppressed because it is too large Load Diff