Files
StarPilot/openpilot/starpilot/galaxy/server.py
T
2026-10-31 18:19:40 -05:00

2985 lines
144 KiB
Python

"""Galaxy settings, device services and software management."""
import argparse
import hashlib
from contextlib import nullcontext
from http.cookies import SimpleCookie
import json
import os
import re
import secrets
import select
import socket
from pathlib import Path
import threading
import time
from urllib.parse import parse_qs, quote, unquote, urlsplit
from openpilot.common.swaglog import cloudlog
from openpilot.starpilot.galaxy.batch_routes import change_drive_state, delete_records, non_default_settings, preview_layout, preview_settings_recovery
from openpilot.starpilot.galaxy.access import AccessStatus, default_owner
from openpilot.starpilot.galaxy.auth import LocalSessions
from openpilot.starpilot.galaxy.http_server import (_LocalHTTPHandler, _LocalHTTPServer, allowed_authority,
direct_local_connection, install_origin_allowed, static_asset)
from openpilot.starpilot.galaxy.camera_snapshot import CameraSnapshot, SnapshotDenied, SnapshotUnavailable
from openpilot.starpilot.galaxy.remote import default_remote_pairing, gateway_cookie_valid
from openpilot.starpilot.galaxy.crash_reports import CrashChanged, CrashMissing, CrashReports, CrashUnavailable
from openpilot.starpilot.galaxy.device_name import DeviceName
from openpilot.starpilot.galaxy.device_state import DeviceStateSource
from openpilot.starpilot.galaxy.recording_library import RecordingLibrary
from openpilot.starpilot.galaxy.drive_history import DriveHistory, DriveHistoryUnavailable, SegmentInUse, SegmentMissing, recording_details
from openpilot.starpilot.galaxy.drive_stats import DriveStatsOwner
from openpilot.starpilot.galaxy.recording_media import RecordingMedia, RecordingMediaUnavailable
from openpilot.starpilot.galaxy.recording_http import camera_video, send_recording
from openpilot.starpilot.galaxy.replay_http import ReplayHTTP, ReplayHTTPError
from openpilot.starpilot.galaxy.route_inventory import CatalogError
from openpilot.starpilot.galaxy.segment_summary import SegmentSummary, SegmentSummaryChanged, SegmentSummaryUnavailable
from openpilot.starpilot.galaxy.flm_training_results import FlmTrainingResults
from openpilot.starpilot.galaxy.sentry_events import SentryEvents, SentryEventsUnavailable
from openpilot.starpilot.sentry_mode.notifications import NotificationOwner, NotificationUnavailable
from openpilot.starpilot.galaxy.map_status import MapStatus, MapUnavailable
from openpilot.starpilot.galaxy.map_operations import MapOperationError, MapOperations, validate_action
from openpilot.starpilot.galaxy.plots import Plots
from openpilot.starpilot.galaxy.software_status import SoftwareStatus, SoftwareUnavailable
from openpilot.starpilot.galaxy.web_manifest import web_manifest
from openpilot.starpilot.galaxy.software_operations import SoftwareOperations, SoftwareOperationError
from openpilot.starpilot.galaxy.settings import PAGES, SettingsGateway, SettingsChanged, SettingsUnavailable
from openpilot.starpilot.galaxy.onroad_layout import LayoutChanged, OnroadLayoutOwner
from openpilot.starpilot.ui.layout_preview_transport import (request_profile)
from openpilot.starpilot.favorites.owner import FavoritesChanged
from openpilot.starpilot.galaxy.favorites import FavoritesGateway
from openpilot.starpilot.galaxy.system_monitor import SystemMonitor
from openpilot.starpilot.galaxy.local_access import LocalAccess
from openpilot.starpilot.galaxy.tmux_live import TmuxLive
from openpilot.starpilot.galaxy.vehicle_selection import VehicleSelectionChanged, VehicleSelectionGateway, VehicleSelectionUnverified
from openpilot.starpilot.bluetooth.owner import BluetoothOwner, BluetoothRejected, BluetoothUnavailable, normalized_address
from openpilot.starpilot.controllers.transport import (ControllerBusy, ControllerDenied, ControllerInvalid,
ControllerUnavailable, request_action as controller_action,
request_status as controller_status)
from openpilot.starpilot.models.runtime import ModelStatusSource
from openpilot.starpilot.audio.downloads import SoundDownloads, SoundDownloadError
WEB = Path(__file__).resolve().parent / 'web'
INSTANCE = secrets.token_hex(8)
def phone_claim_view():
from openpilot.starpilot.galaxy_bluetooth.bridge import claim
return claim.view(claim.CLAIM_PATH)
def link_music_phone(address):
"""Music paired in the window an app claimed (either order): remember which device is the app's phone."""
try:
from openpilot.starpilot.galaxy_bluetooth.bridge import claim, pairing
claim.note_paired(address)
linked = claim.take_link()
if linked:
pairing.link_address(*linked)
except (OSError, ValueError):
pass
def forget_app_keys(address):
"""Forgetting a Bluetooth device also forgets the app key that was paired with it."""
try:
from openpilot.starpilot.galaxy_bluetooth.bridge import pairing
pairing.remove_by_address(address)
except (OSError, ValueError):
pass
def make_server(*, port=8082, host='127.0.0.1', monitor=None, owner=None, crashes=None, software=None, maps=None, models=None, settings=None,
plots=None, map_operations=None, recordings=None, recording_media=None, segment_summary=None,
sentry_events=None, notifications=None, training_results=None, bluetooth=None, vehicle_selection=None,
model_manager=None, layouts=None, favorites=None,
sounds=None, software_operations=None, drive_stats=None, layout_preview_socket=None, controllers_socket=None,
remote_pairing=None, parked=None, camera_snapshot=None, clock=time.monotonic, starport_setup=None,
starport_client=None, navigation=None, offline_maps=None, drive_state=None, cloud_provider=None, cloud_offroad=None, projection_layout=None,
local_access=None, tmux_live=None, starport_logs=None, hotspot=None, toyota_tools=None, replay_client=None, replay_root=None,
spotify_setup=None, companion=None, phone_location=None, reverse_proxy=None):
from openpilot.starpilot.galaxy.companion import CompanionRegistry, APK_PATH
from openpilot.starpilot.gps.phone import PhoneGpsPublisher
companion_source = companion if companion is not None else CompanionRegistry()
training_results_source = training_results if training_results is not None else FlmTrainingResults()
phone_location_source = phone_location if phone_location is not None else PhoneGpsPublisher()
cloud = cloud_provider
hotspot_source = hotspot
hotspot_source_lock = threading.Lock()
def hotspot_owner():
nonlocal hotspot_source
with hotspot_source_lock:
if hotspot_source is None:
from openpilot.starpilot.galaxy.hotspot import HotspotSettings
hotspot_source = HotspotSettings()
return hotspot_source
def cloud_status():
from openpilot.starpilot.connect.provider import status
return cloud.status() if cloud is not None else status()
def cloud_select(name, revision, authorized):
from openpilot.starpilot.connect.provider import select_provider
return cloud.select(name, revision, authorized) if cloud is not None else select_provider(name, revision, authorized)
address_source = local_access if local_access is not None else LocalAccess(port)
console_source = tmux_live if tmux_live is not None else TmuxLive()
reports = crashes if crashes is not None else CrashReports()
crash_reports_lock = threading.Lock()
starport_logs_source = starport_logs
def starport_logs():
nonlocal starport_logs_source
if starport_logs_source is None:
from openpilot.starpilot.galaxy.starport_logs import StarPortLogs
starport_logs_source = StarPortLogs()
return starport_logs_source
software_source = software
software_operations_source = software_operations
software_lock = threading.Lock()
map_source = maps if maps is not None else MapStatus()
navigation_source = navigation
navigation_lock = threading.Lock()
operations = map_operations if map_operations is not None else MapOperations()
drive_control = drive_state
drive_lock = threading.Lock()
replay_api = ReplayHTTP(replay_client, replay_root)
history = recordings if recordings is not None else DriveHistory()
drive_stats_source = drive_stats
drive_stats_lock = threading.Lock()
recording_library = RecordingLibrary(history)
recordings_lock = threading.Lock()
recording_media_source = recording_media
recording_media_lock = threading.Lock()
summary_source = segment_summary
summary_lock = threading.Lock()
motion_events = sentry_events if sentry_events is not None else SentryEvents()
sentry_events_lock = threading.Lock()
last_simulation = [float("-inf")]
notification_owner = notifications
snapshots = camera_snapshot if camera_snapshot is not None else CameraSnapshot()
access = owner if owner is not None else default_owner()
from openpilot.starpilot.galaxy.reverse_proxy import ReverseProxySettings
proxy_settings = ReverseProxySettings(access.root)
pairing = remote_pairing if remote_pairing is not None else default_remote_pairing()
pairing_authority = None
device_name = DeviceName(pairing.root)
def remote_generation(generation, record):
return hashlib.sha256((generation or b'') + bytes.fromhex(record['session'])).digest()
evidence_source = None
if parked is None:
from openpilot.starpilot.galaxy.evidence import EvidenceSource
evidence_source = EvidenceSource()
monitor_source = monitor if monitor is not None else SystemMonitor(messages=evidence_source)
weather_source = None
weather_lock = threading.Lock()
def weather_status():
nonlocal weather_source
with weather_lock:
if weather_source is None:
from openpilot.starpilot.galaxy.weather import WeatherSource
weather_source = WeatherSource()
server.weather_source = weather_source
return weather_source.sample()
vehicle_display = DeviceStateSource(evidence_source)
def context_source(params):
from openpilot.starpilot.galaxy.settings import LiveContextSource
return LiveContextSource(params, messages=evidence_source, evidence_wait_ms=0) if evidence_source is not None else LiveContextSource(params)
if parked is None:
from openpilot.common.params import Params
try:
pairing_authority = context_source(Params())
parked = pairing_authority.parked
parked()
except BaseException:
if evidence_source is not None:
evidence_source.close()
raise
configuration_allowed = pairing_authority.configuration_allowed if pairing_authority is not None else parked
def cloud_allowed():
from openpilot.common.params import Params
return bool(parked()) and bool(cloud_offroad() if cloud_offroad is not None else Params().get_bool('IsOffroad'))
sessions = LocalSessions(clock)
local_generation = secrets.token_bytes(32)
session_lock = threading.Lock()
sample_lock = threading.Lock()
effect_lock = threading.Lock()
settings_lock = threading.Lock()
models_lock = threading.Lock()
settings_gateway = settings
recovery_gateway = None
vehicle_gateway = vehicle_selection
toyota_source = toyota_tools
model_source = models
model_manager_source = model_manager
plot_source = plots
plots_lock = threading.Lock()
bluetooth_lock = threading.Lock()
bluetooth_source = bluetooth
starport_setup_source = starport_setup
spotify_setup_source = spotify_setup
starport_client_source = starport_client
starport_source_lock = threading.Lock()
starport_registry_source = None
projection_layout_source = projection_layout
layout_source = layouts
layout_lock = threading.Lock()
favorites_source = favorites
favorites_lock = threading.Lock()
sound_source = sounds
sounds_lock = threading.Lock()
def drive_owner():
nonlocal drive_control
with drive_lock:
if drive_control is None:
from openpilot.common.params import Params
from openpilot.starpilot.storage import starpilot_storage_root
from openpilot.starpilot.drive_state.owner import DriveStateOwner
from openpilot.starpilot.drive_state.evidence import PhysicalSource
from openpilot.starpilot.drive_state.control import DriveStateControl
physical = PhysicalSource(evidence_source)
drive_control = DriveStateControl(DriveStateOwner(Params(), starpilot_storage_root() / 'drive-state'), physical)
server.drive_physical_source = physical
return drive_control
def navigation_owner():
nonlocal navigation_source
with navigation_lock:
if navigation_source is None:
from openpilot.starpilot.navigation.owner import NavigationOwner
navigation_source = NavigationOwner()
server.navigation_source = navigation_source
return navigation_source
offline_maps_source = offline_maps
def offline_maps_owner():
nonlocal offline_maps_source
with navigation_lock:
if offline_maps_source is None:
from openpilot.starpilot.navigation.offline_owner import OfflineMapsOwner
offline_maps_source = OfflineMapsOwner(position=lambda: navigation_owner().position_store.read())
return offline_maps_source
def statistics_owner():
nonlocal drive_stats_source
with drive_stats_lock:
if drive_stats_source is None:
from openpilot.common.params import Params
params = Params()
from openpilot.starpilot.drive_state.evidence import PhysicalSource
authority = PhysicalSource(evidence_source)
server.drive_stats_authority = authority
def analysis_allowed():
# Use the effective device lifecycle, including Force Offroad.
authority.allowed() # Refresh paired clocks and the existing evidence snapshot.
return params.get_bool('IsOffroad') and authority.effective() is False
drive_stats_source = DriveStatsOwner(root=history.root, permitted=analysis_allowed,
metric=lambda: params.get_bool('IsMetric'))
server.drive_stats_source = drive_stats_source
return drive_stats_source
def software_owner():
nonlocal software_operations_source
with software_lock:
if software_operations_source is None:
software_operations_source = SoftwareOperations(parked=parked, refresh=evidence_source.refresh if evidence_source is not None else None)
server.software_operations_source = software_operations_source
return software_operations_source
def software_snapshot(*, include_history=True):
result = (software_source if software_source is not None else SoftwareStatus()).snapshot()
if software_source is None or software_operations_source is not None:
operations = software_owner().snapshot() if include_history else software_owner().snapshot(include_history=False)
result = {**result, 'operations': operations}
return result
def sound_owner():
nonlocal sound_source
with sounds_lock:
if sound_source is None:
from openpilot.common.params import Params
authority = context_source(Params())
sound_source = SoundDownloads(parked=authority.parked)
server.sound_authority = authority
server.sound_source = sound_source
return sound_source
def favorites_owner():
nonlocal favorites_source
with favorites_lock:
if favorites_source is None:
from openpilot.common.params import Params
params = Params()
favorites_source = FavoritesGateway(params, context_source(params))
server.favorites_source = favorites_source
return favorites_source
def layout_owner():
nonlocal layout_source
with layout_lock:
if layout_source is None:
from openpilot.common.params import Params
params = Params()
authority = context_source(params)
server.layout_authority = authority
layout_source = OnroadLayoutOwner(params, authority.parked)
return layout_source
def projection_layout_owner():
nonlocal projection_layout_source
with layout_lock:
if projection_layout_source is None:
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.projection_layout import ProjectionLayoutOwner, host_owner
params = Params()
projection_layout_source = host_owner(params, configuration_allowed) or ProjectionLayoutOwner(params, configuration_allowed)
return projection_layout_source
def bluetooth_session_valid(identity):
token, generation = identity
with session_lock:
current = access.current_generation()
if (generation == local_generation or current == generation) and sessions.valid(token, generation):
return True
record = pairing.read()
return record is not None and generation == remote_generation(current, record) and \
(sessions.valid(token, generation) or gateway_cookie_valid(token, record))
def bluetooth_paired(address, name, uuids):
link_music_phone(address)
spotify_owner().adopt_phone(address, name, uuids)
def bluetooth_owner():
nonlocal bluetooth_source
with bluetooth_lock:
if bluetooth_source is None:
from openpilot.common.params import Params
authority = context_source(Params())
bluetooth_source = BluetoothOwner(authority.configuration_allowed, session_valid=bluetooth_session_valid,
on_paired=bluetooth_paired)
server.bluetooth_authority = authority
server.bluetooth_source = bluetooth_source
return bluetooth_source
def starport_setup_owner():
nonlocal starport_setup_source
with bluetooth_lock:
if starport_setup_source is None:
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.starport_setup import StarPortSetup
from openpilot.starpilot.system.starport.settings import enabled as starport_enabled, set_enabled as set_starport_enabled
def bluetooth_ready():
snapshot = bluetooth_owner().snapshot()
return bool(snapshot['available'] and snapshot['powered'] and snapshot['errorCode'] is None)
def install_ready():
runtime = Path(__file__).resolve().parents[1] / 'system/starport'
return ((runtime / 'hw/libstarport_encoder.so').is_file() and (runtime / 'current_car_ui.py').is_file() and
Path('/dev/dri/renderD128').exists())
def service_ready():
try:
return bool(starport_client_owner().call('status')['status'])
except (OSError, RuntimeError, KeyError, TypeError):
return False
def enable_bluetooth(identity):
bluetooth_owner().request('power', enabled=True, session=identity, power_on_for_starport=True)
starport_setup_source = StarPortSetup(parked=configuration_allowed, enabled=lambda: starport_enabled(Params()),
session_valid=bluetooth_session_valid, bluetooth_enabled=bluetooth_ready,
install_ready=install_ready, service_ready=service_ready,
set_enabled=lambda value: set_starport_enabled(Params(), value),
enable_bluetooth=enable_bluetooth)
server.starport_setup_source = starport_setup_source
return starport_setup_source
def spotify_owner():
nonlocal spotify_setup_source
with bluetooth_lock:
if spotify_setup_source is None:
from openpilot.starpilot.galaxy.spotify_setup import SpotifySetup
def paired_devices(identity):
return bluetooth_owner().snapshot(session=identity).get('devices', [])
spotify_setup_source = SpotifySetup(parked=configuration_allowed, session_valid=bluetooth_session_valid,
paired_devices=paired_devices)
server.spotify_setup_source = spotify_setup_source
return spotify_setup_source
def starport_source_registry():
nonlocal starport_registry_source
with starport_source_lock:
if starport_registry_source is None:
from openpilot.starpilot.galaxy.starport_source import GalaxySourceRegistry
starport_registry_source = GalaxySourceRegistry(lambda identity: bluetooth_session_valid(identity) and starport_setup_owner().enabled(), clock)
server.starport_source_registry = starport_registry_source
return starport_registry_source
def starport_client_owner():
nonlocal starport_client_source
with starport_source_lock:
if starport_client_source is None:
from openpilot.starpilot.system.starport.protocol import StarPortClient
starport_client_source = StarPortClient()
return starport_client_source
def plot_status():
nonlocal plot_source
with plots_lock:
if plot_source is None:
plot_source = Plots()
server.plots_source = plot_source
return plot_source.snapshot()
def local_media():
nonlocal recording_media_source
with recording_media_lock:
if recording_media_source is None:
recording_media_source = RecordingMedia(getattr(history, 'root', None))
server.recording_media_source = recording_media_source
return recording_media_source
def local_summary():
nonlocal summary_source
with summary_lock:
if summary_source is None:
summary_source = SegmentSummary(history.root)
return summary_source
def feature_settings():
nonlocal settings_gateway
with settings_lock:
if settings_gateway is None:
from openpilot.common.params import Params
params = Params()
settings_gateway = SettingsGateway(params, context_source(params), starport=starport_client_owner)
server.settings_source = settings_gateway
return settings_gateway
def settings_recovery():
nonlocal recovery_gateway
gateway = feature_settings()
with settings_lock:
if recovery_gateway is None:
from openpilot.starpilot.galaxy.defaults_recovery import DefaultsRecovery
recovery_gateway = DefaultsRecovery(gateway)
return recovery_gateway
def toyota_owner():
nonlocal toyota_source
with settings_lock:
if toyota_source is None:
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.toyota_tools import ToyotaTools
params = Params()
toyota_source = ToyotaTools(params, context_source(params))
server.toyota_tools_source = toyota_source
return toyota_source
def vehicle_choices():
nonlocal vehicle_gateway
with settings_lock:
if vehicle_gateway is None:
from openpilot.common.params import Params
params = Params()
vehicle_gateway = VehicleSelectionGateway(params, context_source(params))
server.vehicle_selection_source = vehicle_gateway
return vehicle_gateway
def model_status():
nonlocal model_source
with models_lock:
if model_source is None:
from openpilot.common.params import Params
model_source = ModelStatusSource(Params())
server.model_source = model_source
return model_source.json()
def model_owner():
nonlocal model_manager_source
with models_lock:
if model_manager_source is None:
from openpilot.common.params import Params
from openpilot.starpilot.models.manager import ModelManager
authority = context_source(Params())
model_manager_source = ModelManager(parked=authority.parked, refresh_catalog=True)
server.model_authority = authority
server.model_manager_source = model_manager_source
return model_manager_source
def model_laboratory():
from openpilot.starpilot.models.laboratory import ModelLaboratory
return ModelLaboratory(model_owner())
def login(password):
# Keep throttling, verifier work and session creation in one transaction.
# No socket reads or writes run under this lock.
with session_lock:
if access.status().status != AccessStatus.CONFIGURED_LOCAL:
return 503, {'error': 'Local Galaxy access is unavailable'}, None
if sessions.throttled():
return 429, {'error': 'Try again shortly'}, None
generation = access.authenticate_generation(password)
if generation is None and password != password.strip():
generation = access.authenticate_generation(password.strip())
if generation is None:
sessions.failed_login()
return 401, {'error': 'Sign in failed'}, None
return 200, {'authenticated': True}, sessions.create(generation)
class Handler(_LocalHTTPHandler):
def camera_video(self, encoded_name: str, *, combined=False):
camera_video(self, encoded_name, media=local_media, combined=combined)
def send_recording(self, lease, content_type='video/mp4'):
send_recording(self, lease, content_type)
def local_request(self, *, mutation=False):
proxy = reverse_proxy if reverse_proxy is not None else proxy_settings.current
host = self.headers.get('Host', '')
origin = self.headers.get('Origin')
local_address = self.connection.getsockname()[0]
if getattr(self.server, 'remote_transport', False):
record = pairing.read()
valid_host = record is not None and host in (f'{record["slug"]}.devices.local', 'galaxy.firestar.link')
valid_origin = origin is None or origin == 'https://galaxy.firestar.link'
if not valid_host or not valid_origin or mutation and origin != 'https://galaxy.firestar.link':
self.json(403, {'error': 'Remote Galaxy request denied'})
return False
prefix = f'/{record["slug"]}'
if self.path == prefix and self.command in ('GET', 'HEAD'):
self.send_response(308)
self.send_header('Location', prefix + '/')
self.send_header('Content-Length', '0')
self.send_header('Cache-Control', 'no-store')
self.end_headers()
return False
if self.path.startswith(prefix + '/') or self.path == prefix:
self.path = self.path[len(prefix):] or '/'
return True
if proxy is not None and proxy.allows(self.client_address[0], host, origin, mutation=mutation):
return True
if not allowed_authority(host, local_address, self.server.server_port) or \
(mutation and origin != f'http://{host}') or \
(origin is not None and origin != f'http://{host}'):
self.json(403, {'error': 'Local access only'})
return False
return True
def session_token(self):
try:
cookie = SimpleCookie()
cookie.load(self.headers.get('Cookie', ''))
return cookie['galaxy_session'].value if 'galaxy_session' in cookie else None
except Exception:
return None
def credential_state(self):
return access.status().status
def direct_local(self):
proxy = reverse_proxy if reverse_proxy is not None else proxy_settings.current
return not getattr(self.server, 'remote_transport', False) and \
not (proxy is not None and proxy.matches_peer(self.client_address[0])) and \
direct_local_connection(self.client_address[0], self.connection.getsockname()[0], self.headers)
def spotify_install_allowed(self):
proxy = reverse_proxy if reverse_proxy is not None else proxy_settings.current
if proxy is not None and proxy.matches_peer(self.client_address[0]):
return False
return install_origin_allowed(self.client_address[0], self.connection.getsockname()[0], self.headers,
getattr(self.server, 'remote_transport', False))
def authenticated(self):
return self.settings_session() is not None
def require_session(self):
if self.authenticated():
return True
if self.direct_local():
self.json(401, {'error': 'Refresh the local Galaxy session'})
return False
if getattr(self.server, 'remote_transport', False):
self.json(401, {'error': 'Sign in through the Galaxy gateway'})
return False
status = self.credential_state()
if status != AccessStatus.CONFIGURED_LOCAL:
self.json(503, {'error': 'Local Galaxy access is unavailable' if status == AccessStatus.UNAVAILABLE else
'Remote Galaxy access is not configured',
'code': 'access_unavailable' if status == AccessStatus.UNAVAILABLE else 'setup_required'})
return False
self.json(401, {'error': 'Sign in to Galaxy'})
return False
def settings_session(self):
token = self.session_token()
with session_lock:
if self.direct_local() and sessions.valid(token, local_generation):
return token, local_generation
generation = access.current_generation()
if getattr(self.server, 'remote_transport', False):
record = pairing.read()
if record is None:
return None
paired_generation = remote_generation(generation, record)
if gateway_cookie_valid(token, record):
return token, paired_generation
generation = paired_generation
return (token, generation) if sessions.valid(token, generation) else None
def starport_command(self, identity, command, **arguments):
"""Bind one daemon command to this live Galaxy session."""
if self.settings_session() != identity:
raise ValueError('Galaxy session changed')
source = starport_source_registry().mint(identity)
try:
if self.settings_session() != identity:
raise ValueError('Galaxy session changed')
return starport_client_owner().call(command, source=source, **arguments)
finally:
starport_source_registry().revoke_source(source)
def phone_claim(self, operation, result):
"""The same window that lets a phone pair for music also lets the StarPilot app claim its key."""
from openpilot.starpilot.galaxy_bluetooth.bridge import claim
try:
if operation == 'cancel_pair':
claim.clear()
elif type(result) is dict:
claim.arm(result.get('adapterAddress', ''), result.get('adapterName', ''))
except OSError:
pass
def starport_phone_window(self, identity, operation, payload=None):
"""The phone pairing window inside a running Android Auto projection; handled False when none holds the adapter."""
registry = starport_source_registry()
minted = operation in ('accept_pair', 'pair', 'scan', 'stop_scan')
source = registry.mint(identity) if minted else registry.current(identity)
if source is None:
return {'handled': False}
arguments = {'address': payload['address']} if operation == 'pair' and payload is not None else {}
if operation == 'pairing_response' and payload is not None:
arguments = {'prompt_id': payload['promptId'], 'accepted': payload['accepted'], 'value': payload.get('value', '')}
try:
if self.settings_session() != identity:
raise ValueError('Galaxy session changed')
delegated = starport_client_owner().call('phone_window', source=source, operation=operation, **arguments)
except (OSError, RuntimeError) as error:
# Falling back to this process cannot work while Android Auto holds the adapter, so say why it was skipped.
cloudlog.warning(f'android auto phone window {operation} not delegated: {error!r}')
delegated = {'handled': False}
if type(delegated) is not dict or type(delegated.get('handled')) is not bool:
delegated = {'handled': False}
if minted and (not delegated['handled'] or 'error_code' in delegated or operation in ('scan', 'stop_scan')) or \
operation == 'cancel_pair' and delegated['handled']:
registry.revoke_source(source)
found = delegated.get('bluetooth')
paired = found.pop('windowPaired', None) if type(found) is dict else None
if type(paired) is dict:
link_music_phone(paired.get('address', ''))
if type(paired) is dict and configuration_allowed():
try:
spotify_owner().adopt_phone(paired['address'], paired['name'], tuple(paired['uuids']))
except Exception:
pass
return delegated
def do_GET(self):
if not self.local_request():
return
path = urlsplit(self.path).path
if path.startswith(('/api/replay/', '/api/recordings/')) and not self.direct_local():
self.json(403, {'error': 'Routes require a local Galaxy connection', 'code': 'local_only'})
return
if path.startswith('/api/replay/'):
replay_api.get(self, path)
return
if path.startswith('/api/recordings/route/'):
if urlsplit(self.path).query:
self.json(400, {'error': 'Unexpected media query'})
else:
self.camera_video(path.removeprefix('/api/recordings/route/'), combined=True)
return
if path.startswith('/api/recordings/media/'):
if urlsplit(self.path).query:
self.json(400, {'error': 'Unexpected media query'})
else:
self.camera_video(path.removeprefix('/api/recordings/media/'))
return
if path == '/api/connect/proxy':
if not self.require_session():
return
try:
self.json(200, {**proxy_settings.snapshot(), 'canEdit': cloud_allowed(),
'passwordConfigured': access.status().status == AccessStatus.CONFIGURED_LOCAL})
except (OSError, ValueError):
self.json(503, {'error': 'Reverse proxy configuration is unavailable'})
return
if path == '/api/connect/provider':
if not self.require_session():
return
try:
result = cloud_status()
result['canSelect'] = cloud_allowed()
except (OSError, ValueError, TypeError):
self.json(503, {'error': 'Cloud provider configuration is unavailable'})
else:
self.json(200, result)
return
if path == '/api/auth/session':
if self.direct_local():
with session_lock:
token = self.session_token()
if not sessions.valid(token, local_generation):
token = sessions.create(local_generation, reset_failures=False)
self.respond(200, b'{"authenticated":true,"state":"configured","localAccess":true}',
cookie=f'galaxy_session={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age={sessions.LIFETIME}')
return
if getattr(self.server, 'remote_transport', False):
self.json(200, {'authenticated': self.authenticated(), 'localAccess': False, 'gatewayAccess': True,
'state': 'configured' if pairing.read() is not None else 'setup_required'})
return
status = self.credential_state()
self.json(200, {'authenticated': status == AccessStatus.CONFIGURED_LOCAL and self.authenticated(),
'localAccess': False,
**({'gatewayAccess': True} if getattr(self.server, 'remote_transport', False) else {}),
'state': 'configured' if status == AccessStatus.CONFIGURED_LOCAL else
'unavailable' if status == AccessStatus.UNAVAILABLE else 'setup_required'})
elif path == '/api/localization':
if not self.require_session():
return
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.localization import language_snapshot
previous = parse_qs(urlsplit(self.path).query).get('language', [''])[0]
self.json(200, language_snapshot(Params(), previous))
elif path == '/api/galaxy/device-name':
if not self.require_session():
return
try:
result = {'name': device_name.read()}
except (OSError, ValueError, UnicodeError):
self.json(503, {'error': 'The saved comma name could not be read'})
else:
if self.require_session():
self.json(200, result)
elif path in ('/app.apk', '/api/companion/app.apk'):
if getattr(self.server, 'remote_transport', False):
self.json(403, {'error': 'Download the app over local Wi-Fi'})
return
if not self.direct_local() and not self.require_session():
return
try:
handle = APK_PATH.open('rb')
except OSError:
self.json(404, {'error': 'Companion APK is not installed'})
return
with handle:
self.send_response(200)
self.send_header('Content-Type', 'application/vnd.android.package-archive')
self.send_header('Content-Length', str(os.fstat(handle.fileno()).st_size))
self.send_header('Content-Disposition', 'attachment; filename="starpilot-companion.apk"')
self.send_header('Cache-Control', 'no-store')
self.end_headers()
if self.command != 'HEAD':
while chunk := handle.read(65536):
self.wfile.write(chunk)
elif path == '/api/companion/status':
if self.require_session():
try:
self.json(200, companion_source.snapshot())
except (OSError, ValueError):
self.json(503, {'error': 'Companion settings unavailable'})
elif path == '/api/galaxy/hotspot':
if not self.require_session():
return
try:
result = hotspot_owner().snapshot(parked())
except (OSError, ValueError):
self.json(503, {'error': 'Hotspot settings are unavailable.'})
else:
if self.require_session():
self.json(200, result)
elif path in ('/api/galaxy/session', '/api/galaxy/session/qr.svg'):
record = pairing.read()
remote = getattr(self.server, 'remote_transport', False)
if not self.direct_local() and not (remote and record and gateway_cookie_valid(self.session_token(), record)):
self.json(403, {'error': 'A direct connection or matching gateway cookie is required'})
return
if not self.require_session():
return
if record is None:
self.json(200, {'paired': False})
return
token = quote(f"{record['slug']}:{record['session']}", safe='')
if path.endswith('/qr.svg'):
from openpilot.common.qrcode import _Qr, _capacity
raw = ('starpilot-remote://add?key=' + token).encode()
version = next(version for version in range(1, 21) if 4 + 8 + len(raw) * 8 <= _capacity(version) * 8)
matrix = _Qr(version, raw).modules
size = len(matrix) + 8
cells = ''.join(f'<path d="M{x + 4} {y + 4}h1v1h-1z"/>' for y, row in enumerate(matrix) for x, active in enumerate(row) if active)
svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}">' +
f'<rect width="100%" height="100%" fill="white"/><g fill="black">{cells}</g></svg>')
self.respond(200, svg.encode(), 'image/svg+xml', cache_control='no-store')
else:
self.json(200, {'paired': True, 'slug': record['slug'], 'url': pairing.url(record['slug']),
'cookieName': 'galaxy_session', 'sessionToken': token})
elif path == '/api/galaxy/status':
if not self.require_session():
return
if pairing_authority is not None:
parked()
from openpilot.starpilot.galaxy.remote_service import frpc_binary
record = pairing.read()
legacy_available = access.legacy_available()
self.json(200, {'paired': record is not None, 'url': pairing.url(record['slug']) if record else '',
'tunnelClientAvailable': frpc_binary() is not None,
'legacyPassword': access.status().status == AccessStatus.LEGACY_IMPORT_AVAILABLE,
'legacyPairingAvailable': legacy_available is True})
elif path == '/api/galaxy/qr.svg':
if not self.require_session():
return
record = pairing.read()
if record is None:
self.json(404, {'error': 'Galaxy is not paired'})
return
from openpilot.common.qrcode import _Qr, _capacity
raw = pairing.url(record['slug']).encode()
version = next(version for version in range(1, 21)
if 4 + 8 + len(raw) * 8 <= _capacity(version) * 8)
matrix = _Qr(version, raw).modules
size = len(matrix) + 8
cells = ''.join(f'<path d="M{x + 4} {y + 4}h1v1h-1z"/>' for y, row in enumerate(matrix)
for x, active in enumerate(row) if active)
svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}" shape-rendering="crispEdges">' +
f'<rect width="100%" height="100%" fill="white"/><g fill="black">{cells}</g></svg>')
self.respond(200, svg.encode(), 'image/svg+xml')
elif path in ('/data/runtime.json', '/api/companion/runtime'):
self.json(200, {'schemaVersion': 1, 'monitor': 'local', 'instance': INSTANCE})
elif path == '/api/companion/web-manifest':
self.json(200, web_manifest(WEB))
elif path == '/api/companion/bluetooth-ready':
if not self.require_session():
return
from openpilot.starpilot.galaxy.pairing_ready import bridge_state, pairing_readiness
self.json(200, pairing_readiness(parked=configuration_allowed, snapshot=lambda: bluetooth_owner().snapshot(), bridge=bridge_state))
elif path in ('/api/local-access', '/api/tmux/live', '/api/troubleshoot'):
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if path == '/api/local-access':
result = address_source.snapshot()
elif path == '/api/tmux/live':
result = console_source.snapshot()
else:
result = feature_settings().diagnostics()
result['device'] = vehicle_display.sample()
except (OSError, RuntimeError, ValueError, SettingsUnavailable):
self.json(503, {'error': 'Device diagnostics could not be read. Refresh or open System Monitor.'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/weather':
if not self.require_session():
return
result = weather_status()
if self.require_session():
self.json(200, result)
elif path == '/api/system/monitor':
if not self.require_session():
return
try:
# CPU deltas and PID reuse tracking have one shared sample history.
with sample_lock:
result = monitor_source.sample()
except OSError:
self.json(503, {'error': 'System telemetry is unavailable'})
else:
self.json(200, result)
elif path == '/api/crash-reports' or path.startswith('/api/crash-reports/'):
if not self.require_session():
return
try:
result = reports.list() if path == '/api/crash-reports' else reports.preview(path.removeprefix('/api/crash-reports/'))
except CrashMissing:
self.json(404, {'error': 'Crash report is no longer available'})
except CrashChanged:
self.json(409, {'error': 'Crash report changed; refresh the list'})
except CrashUnavailable:
self.json(503, {'error': 'Crash reports are unavailable'})
else:
# A session can be revoked while a directory scan or file read runs.
# Keep filesystem work outside the session lock, then check again
# immediately before sending potentially sensitive content.
if self.require_session():
self.json(200, result)
elif path == '/api/starport/logs' or path.startswith('/api/starport/logs/'):
if not self.require_session():
return
from openpilot.starpilot.galaxy.starport_logs import LogMissing, LogsUnavailable
name = unquote(path.removeprefix('/api/starport/logs/file/')) if path.startswith('/api/starport/logs/file/') else None
if name is not None and not re.fullmatch(r'[\w.-]{1,128}', name):
self.json(400, {'error': 'Invalid log name'})
return
if path not in ('/api/starport/logs', '/api/starport/logs/bundle') and name is None:
self.json(404, {'error': 'Not found'})
return
try:
if path == '/api/starport/logs':
result = starport_logs().list()
elif name is None:
result = starport_logs().bundle()
else:
result = starport_logs().file(name)
except LogMissing:
self.json(404, {'error': 'That log is no longer kept; refresh the list'})
except LogsUnavailable:
self.json(503, {'error': 'Android Auto logs are unavailable'})
else:
# The session can be revoked while logs are read; check again right before sending them.
if not self.require_session():
return
if path == '/api/starport/logs':
self.json(200, result)
else:
filename, body = result
self.attachment(filename, body, 'application/zip' if filename.endswith('.zip') else 'text/plain; charset=utf-8')
elif path.startswith('/api/navigation/map/tiles/'):
if not self.require_session():
return
match = re.fullmatch(r'/api/navigation/map/tiles/([0-9]{1,2})/([0-9]{1,6})/([0-9]{1,6})\.png', path)
if match is None:
self.json(400, {'error': 'Invalid map tile'})
return
try:
query = parse_qs(urlsplit(self.path).query, keep_blank_values=True, max_num_fields=1)
theme = query.get('theme', ['light'])[0]
if set(query) - {'theme'} or theme not in ('light', 'dark'):
raise ValueError('Invalid map theme')
except ValueError:
self.json(400, {'error': 'Invalid map theme'})
return
try:
tile = navigation_owner().map_tile(*(int(value) for value in match.groups()), theme)
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'Map tiles are unavailable'})
else:
if self.require_session():
# Map imagery is not private data; let the browser keep it while panning back and forth.
self.respond(200, tile, 'image/png', cache_control='private, max-age=900')
elif path == '/api/navigation/offline':
if not self.require_session():
return
try:
result = offline_maps_owner().snapshot()
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'Offline maps are unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path == '/api/navigation/status':
if not self.require_session():
return
# A poll that already holds the drawn route sends its key and skips the route lines.
try:
route_key = parse_qs(urlsplit(self.path).query, max_num_fields=1).get('routeKey', [''])[0]
except ValueError:
route_key = ''
try:
result = navigation_owner().snapshot(route_key if re.fullmatch(r'[0-9a-f]{16}', route_key) else None)
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'Navigation status is unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path == '/api/software/status':
identity = self.settings_session()
if identity is None:
if not self.require_session():
return
try:
result = software_snapshot(include_history=parse_qs(urlsplit(self.path).query).get('history') != ['0'])
except (SoftwareUnavailable, SoftwareOperationError, OSError):
self.json(503, {'error': 'Software status is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, {**result, 'instance': INSTANCE})
else:
if self.require_session():
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/bluetooth/status':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = bluetooth_owner().snapshot(session=identity)
result['claim'] = phone_claim_view()
if result.get('pairing') is None and starport_setup_owner().enabled():
delegated = self.starport_phone_window(identity, 'status')
if delegated['handled'] and type(delegated.get('bluetooth')) is dict:
result['pairing'] = delegated['bluetooth'].get('pairing')
except (OSError, RuntimeError):
self.json(503, {'error': 'Bluetooth is unavailable', 'code': 'unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/starport/setup':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = starport_setup_owner().status(identity)
except Exception:
self.json(503, {'error': 'StarPort setup is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/spotify/setup':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = {**spotify_owner().status(identity), 'uploadAllowed': self.spotify_install_allowed()}
except Exception:
self.json(503, {'error': 'Spotify setup is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/starport/pairing/status':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
source = starport_source_registry().current(identity)
try:
result: dict = starport_client_owner().call('pairing_status', source=source) if source and configuration_allowed() else \
{'pairing': {'active': False, 'receiver': None, 'prompt': None, 'approved': False}}
except (OSError, RuntimeError):
self.json(503, {'error': 'StarPort pairing service is unavailable'})
else:
try:
runtime = starport_client_owner().call('status')['status']
except (OSError, RuntimeError, ValueError, KeyError, TypeError):
runtime = None
result['selectedReceiver'] = None
if runtime is not None and runtime.get('receiver_address'):
try:
address = normalized_address(runtime['receiver_address'])
result['selectedReceiver'] = {'address': address, 'name': str(runtime.get('receiver_name') or address)[:80]}
except ValueError:
pass
result['runtime'] = runtime
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/starport/receivers':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
runtime = starport_client_owner().call('status')['status']
if runtime.get('pairing_ready'):
raise ValueError('Finish or cancel pairing before choosing a car')
if not starport_setup_owner().enabled():
raise ValueError('Enable StarPort first')
result = self.starport_command(identity, 'devices')
devices = [{'address': normalized_address(item['address']), 'name': str(item['name'])[:80]}
for item in result['devices'] if item.get('paired') and item.get('starport')]
except ValueError as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError, KeyError, TypeError):
self.json(503, {'error': 'Paired Android Auto cars are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, {'receivers': devices})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path.startswith('/api/starport/source/'):
source_id = path.removeprefix('/api/starport/source/')
# The daemon queries this short-lived opaque source over loopback.
if getattr(self.server, 'remote_transport', False) or self.client_address[0] != '127.0.0.1' or \
self.connection.getsockname()[0] != '127.0.0.1' or not self.direct_local():
self.json(403, {'valid': False})
elif starport_source_registry().valid(source_id):
self.json(200, {'valid': True})
else:
self.json(410, {'valid': False})
elif path == '/api/controllers/status':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
layout_owner().parked()
result = controller_status(controllers_socket)
except (ControllerUnavailable, ControllerInvalid, OSError):
self.json(503, {'error': 'Controller buttons are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/toyota-tools':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = toyota_owner().page(*identity)
except (OSError, RuntimeError, ValueError):
self.json(503, {'error': 'Toyota settings are unavailable'})
else:
self.json(200, result) if self.settings_session() == identity else self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/vehicle-selection':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = vehicle_choices().page(*identity)
except (OSError, RuntimeError, ValueError):
if self.settings_session() == identity:
self.json(503, {'error': 'Vehicle selection is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/drives/stats':
if not self.require_session():
return
try:
query = parse_qs(urlsplit(self.path).query, keep_blank_values=True, max_num_fields=1)
if query and set(query) != {'timezone'}:
raise ValueError('Invalid timezone query')
result = statistics_owner().snapshot(timezone=query['timezone'][0]) if query else statistics_owner().snapshot()
except ValueError:
if self.require_session():
self.json(400, {'error': 'Invalid timezone'})
except (OSError, RuntimeError):
if self.require_session():
self.json(503, {'error': 'Driving history is unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path == '/api/drive-state/status':
if self.require_session():
result = drive_owner().snapshot()
if self.require_session():
self.json(200, result)
elif path == '/api/device/state':
if self.require_session():
state = vehicle_display.sample()
if self.require_session():
self.json(200, state)
elif path.startswith('/api/recordings/logs/'):
if not self.require_session():
return
try:
with recordings_lock:
lease = recording_library.open_archive(unquote(path.removeprefix('/api/recordings/logs/')), permitted=self.authenticated)
except (OSError, ValueError, RecordingMediaUnavailable, DriveHistoryUnavailable):
self.json(404, {'error': 'Drive logs unavailable; download individual segment logs'})
else:
self.send_recording(lease, 'application/x-tar')
elif path.startswith('/api/recordings/files/'):
if not self.require_session():
return
name, _, filename = unquote(path.removeprefix('/api/recordings/files/')).partition('/')
if filename not in {'rlog.zst', 'rlog.bz2', 'qlog.zst', 'qlog.bz2'}:
self.json(400, {'error': 'Invalid log file'})
return
try:
lease = recording_library.open_log(name, filename)
except (OSError, ValueError, RecordingMediaUnavailable):
self.json(404, {'error': 'Closed recording log unavailable'})
else:
self.send_recording(lease, 'application/octet-stream')
elif path == '/api/recordings/detail':
if not self.require_session():
return
try:
query = parse_qs(urlsplit(self.path).query, keep_blank_values=True, max_num_fields=1)
if set(query) != {'routeId'} or len(query['routeId']) != 1:
raise ValueError('Choose one route')
detail = history.detail(query['routeId'][0])
from openpilot.starpilot.galaxy.recording_media import MAX_SOURCE_BYTES
detail['footageDownloads'] = {camera: detail['inventoryComplete'] and 0 < size <= MAX_SOURCE_BYTES
for camera, size in detail['videoBytes'].items()}
log_bytes = sum(sum(segment.get('logBytes', {}).values()) for segment in detail['segments'])
detail['logArchiveAvailable'] = detail['inventoryComplete'] and 0 < log_bytes <= MAX_SOURCE_BYTES
detail['segments'] = [{key: value for key, value in segment.items() if key != 'sourceFiles'}
for segment in detail['segments']]
except (CatalogError, ValueError, DriveHistoryUnavailable) as error:
if self.require_session():
self.json(409, {'error': str(error) or 'Route inventory unavailable'})
else:
if self.require_session():
self.json(200, {'schemaVersion': 1, 'source': 'local', 'route': detail})
elif path == '/api/recordings/local':
if not self.require_session():
return
try:
with recordings_lock:
result = recording_library.describe(history.snapshot())
dongle, dates, device_ids = None, {}, {}
try:
from openpilot.common.params import Params
from openpilot.starpilot.saved_source import read_saved
from openpilot.starpilot.connect.provider import PROVIDERS, recording_device_id
params = Params()
device_ids = {name: recording_device_id(name, params) for name in PROVIDERS}
raw, readable = read_saved(params, 'DongleId', 32)
dongle = raw.decode('ascii', errors='replace') if readable and raw is not None else None
dates = statistics_owner().recording_dates(route['routeId'] for route in result['routes'])
except (OSError, RuntimeError, AttributeError, ValueError):
pass # Optional metadata must not hide usable local recordings.
result = recording_details(result, dates, dongle, device_ids=device_ids)
except DriveHistoryUnavailable:
if self.require_session():
self.json(503, {'error': 'Local recordings are unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path == '/api/recordings/segment-summary':
if not self.require_session():
return
try:
query = parse_qs(urlsplit(self.path).query, keep_blank_values=True, max_num_fields=2)
if set(query) != {'segmentName'} or len(query['segmentName']) != 1:
raise ValueError('Invalid segment selection')
result = local_summary().snapshot(query['segmentName'][0], permitted=lambda: True)
except ValueError:
if self.require_session():
self.json(400, {'error': 'Invalid segment selection'})
except SegmentSummaryChanged:
if self.require_session():
self.json(409, {'error': 'Recording changed; refresh the inventory'})
except SegmentSummaryUnavailable:
if self.require_session():
self.json(503, {'error': 'Recorded segment details are unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path == '/api/flm/training-results':
if not self.require_session():
return
if urlsplit(self.path).query:
self.json(400, {'error': 'Unexpected training-results query'})
return
result = training_results_source.snapshot()
if self.require_session():
self.json(200, result)
elif path.startswith('/api/flm/'):
if self.require_session():
self.json(404, {'error': 'Not found'})
elif path == '/api/sentry/notifications':
if not self.require_session():
return
try:
result = notification_owner.snapshot()
except NotificationUnavailable:
if self.require_session():
self.json(503, {'error': 'Sentry notifications are unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path.startswith('/api/sentry/image/'):
if not self.require_session():
return
parts = path.removeprefix('/api/sentry/image/').split('/')
if len(parts) != 2:
self.json(400, {'error': 'Choose an event image'})
return
try:
body = motion_events.image(parts[0], parts[1])
except SentryEventsUnavailable:
self.json(404, {'error': 'Event image unavailable'})
else:
if self.require_session():
self.respond(200, body, 'image/jpeg')
elif path == '/api/sentry/events':
if not self.require_session():
return
try:
with sentry_events_lock:
result = motion_events.snapshot()
except SentryEventsUnavailable:
if self.require_session():
self.json(503, {'error': 'Local motion events are unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path in ('/api/maps/status', '/api/maps/catalog', '/api/maps/operation', '/api/maps/setup'):
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = map_source.snapshot() if path.endswith('/status') else operations.request(
'catalog' if path.endswith('/catalog') else 'setup' if path.endswith('/setup') else 'status')
except (MapUnavailable, MapOperationError) as error:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
elif isinstance(error, MapOperationError):
self.json(error.status, {'error': 'Map management is unavailable', 'code': error.code})
else:
self.json(503, {'error': 'Map observation is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/sounds':
if not self.require_session():
return
try:
result = sound_owner().snapshot()
except SoundDownloadError as error:
self.json(error.status, {'error': str(error)})
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'Sound packs are unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path in ('/api/models/status', '/api/models/manager', '/api/models/laboratory'):
if not self.require_session():
return
try:
result = model_status() if path.endswith('/status') else (
model_laboratory().snapshot() if path.endswith('/laboratory') else model_owner().snapshot())
except (OSError, ValueError, RuntimeError):
if self.require_session():
self.json(503, {'error': 'Model status is unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path == '/api/plots/live':
if not self.require_session():
return
try:
result = plot_status()
except (OSError, ValueError, RuntimeError):
if self.require_session():
self.json(503, {'error': 'Live plots are unavailable'})
else:
if self.require_session():
self.json(200, result)
elif path == '/api/favorites/slots':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = favorites_owner().snapshot()
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'Saved Favorites are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/starport/layout':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = projection_layout_owner().snapshot()
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'StarPort layout is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/ui/layout':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = layout_owner().snapshot()
result['activeProfile'] = request_profile(layout_preview_socket)
if result['activeProfile'] is None:
from openpilot.common.hardware import HARDWARE
result['activeProfile'] = {'mici': 'compact', 'tici': 'large', 'tizi': 'large'}.get(HARDWARE.get_device_type())
result['supportedProfiles'] = [result['activeProfile']] if result['activeProfile'] is not None else []
if not result['supportedProfiles']:
result['editable'] = False
try:
projection_status = projection_layout_owner().snapshot()
result['projectionAvailable'] = True
result['projectionReason'] = projection_status.get('reason')
except (OSError, ValueError, RuntimeError):
result['projectionAvailable'] = False
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'Saved layout is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/settings/backup':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
from openpilot.starpilot.galaxy.toggle_backup import ToggleBackup
result = ToggleBackup(feature_settings(), layout_owner()).export()
except (SettingsUnavailable, SettingsChanged, OSError, RuntimeError, ValueError):
self.json(503, {'error': 'Toggle backup is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/settings/non-defaults':
non_default_settings(self, settings_recovery)
elif path.startswith('/api/settings/pages/'):
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
result = feature_settings().page(unquote(path.removeprefix('/api/settings/pages/')), *identity)
except SettingsUnavailable:
self.json(404, {'error': 'Settings page unavailable'})
except SettingsChanged:
if self.settings_session() == identity:
self.json(409, {'error': 'Saved settings changed; refresh the page'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
except (OSError, ValueError, RuntimeError):
if self.require_session():
self.json(503, {'error': 'Saved settings are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path.startswith('/api/'):
if self.require_session():
self.json(404, {'error': 'Capability unavailable'})
else:
try:
target = (WEB / unquote(path).lstrip('/')).resolve()
if path == '/':
target = WEB / 'index.html'
if not target.is_relative_to(WEB) or not target.is_file():
self.json(404, {'error': 'Page unavailable'})
return
stat = target.stat()
body, packed, content_type = static_asset(str(target), (stat.st_ino, stat.st_mtime_ns, stat.st_size))
except (OSError, ValueError):
self.json(404, {'error': 'Page unavailable'})
return
encodings = [entry.strip() for entry in self.headers.get('Accept-Encoding', '').lower().split(',')]
compressed = 'gzip' in encodings and packed is not body
payload = packed if compressed else body
etag = '"' + hashlib.sha256(payload).hexdigest() + '"'
headers = {'ETag': etag, 'Vary': 'Accept-Encoding'}
if compressed:
headers['Content-Encoding'] = 'gzip'
unchanged = self.headers.get('If-None-Match') == etag
self.respond(304 if unchanged else 200, b'' if unchanged else payload, content_type, asset_headers=headers)
def do_HEAD(self):
self.do_GET()
def do_POST(self):
if not self.local_request(mutation=True):
return
path = urlsplit(self.path).path
if path.startswith(('/api/replay/', '/api/recordings/')) and not self.direct_local():
self.json(403, {'error': 'Routes require a local Galaxy connection', 'code': 'local_only'})
return
if path == '/api/starport/upload':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if self.headers.get('Transfer-Encoding') is not None or \
self.headers.get('Content-Type', '').lower() != 'application/octet-stream':
self.json(415, {'error': 'Binary APK/XAPK upload required'})
return
try:
size = int(self.headers.get('Content-Length', ''))
except ValueError:
size = -1
from openpilot.starpilot.system.starport.apk_identity import MAX_FILE_BYTES
if not 0 < size <= MAX_FILE_BYTES:
self.json(413, {'error': 'StarPort package size is invalid'})
return
from openpilot.starpilot.galaxy.starport_setup import SetupRejected
old_timeout = self.connection.gettimeout()
try:
self.connection.settimeout(30.0)
result = starport_setup_owner().upload(identity, self.rfile, size)
except SetupRejected as error:
self.json(409, {'error': str(error)})
except (OSError, TimeoutError):
self.json(503, {'error': 'Android Auto upload failed'})
else:
if self.settings_session() == identity:
self.json(202, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
finally:
self.connection.settimeout(old_timeout)
return
if path == '/api/spotify/upload':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if not self.spotify_install_allowed():
self.json(403, {'error': 'Install Spotify over your local network or the Galaxy tunnel'})
return
if self.headers.get('Transfer-Encoding') is not None or \
self.headers.get('Content-Type', '').lower() != 'application/octet-stream':
self.json(415, {'error': 'Binary .tar.gz upload required'})
return
try:
size = int(self.headers.get('Content-Length', ''))
except ValueError:
size = -1
from openpilot.starpilot.soloist.package import MAX_UPLOAD_BYTES
if not 0 < size <= MAX_UPLOAD_BYTES:
self.json(413, {'error': 'Soloist download size is invalid'})
return
from openpilot.starpilot.galaxy.spotify_setup import SetupRejected as SpotifyRejected
old_timeout = self.connection.gettimeout()
try:
self.connection.settimeout(30.0)
result = spotify_owner().upload(identity, self.rfile, size)
except SpotifyRejected as error:
self.json(409, {'error': str(error)})
except (OSError, TimeoutError):
self.json(503, {'error': 'Soloist upload failed'})
else:
if self.settings_session() == identity:
self.json(202, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
finally:
self.connection.settimeout(old_timeout)
return
if path.startswith('/api/flm/'):
self.close_connection = True
if self.require_session():
self.json(404, {'error': 'Not found'})
return
if path not in ('/api/companion/uplink', '/api/companion/settings', '/api/companion/location',
'/api/connect/proxy', '/api/connect/provider', '/api/auth/login', '/api/auth/logout', '/api/settings/preview', '/api/settings/confirm',
'/api/settings/recovery/preview', '/api/settings/recovery/confirm',
'/api/settings/reset-default', '/api/settings/restore', '/api/recordings/action',
'/api/replay/start', '/api/replay/control', '/api/replay/stop',
'/api/replay/export-start',
'/api/galaxy/pair', '/api/galaxy/unpair', '/api/galaxy/device-name', '/api/galaxy/hotspot', '/api/cameras/snapshot',
'/api/starport/layout', '/api/starport/enable', '/api/starport/control', '/api/starport/pairing',
'/api/starport/pairing/response', '/api/starport/pairing/cancel', '/api/starport/pairing/select',
'/api/spotify/enable', '/api/spotify/key', '/api/spotify/preferences', '/api/spotify/control', '/api/spotify/maintenance',
'/api/ui/layout', '/api/ui/layout/preview', '/api/favorites/slots',
'/api/maps/start', '/api/maps/cancel',
'/api/bluetooth/action', '/api/controllers/action',
'/api/models/active', '/api/models/preferences', '/api/models/download', '/api/models/download_all',
'/api/models/cancel', '/api/models/delete', '/api/models/refresh_manifest', '/api/models/jetlink',
'/api/models/laboratory', '/api/models/laboratory/download', '/api/models/laboratory/delete',
'/api/sounds/download', '/api/sounds/cancel', '/api/software/action', '/api/drives/ignore',
'/api/recordings/delete-videos', '/api/sentry/notifications', '/api/sentry/events/delete', '/api/crash-reports/delete',
'/api/navigation/search', '/api/navigation/action', '/api/navigation/offline', '/api/drive-state/action',
'/api/toyota-tools/action', '/api/vehicle-selection/preview', '/api/vehicle-selection/confirm'):
self.json(405, {'error': 'Method unavailable'})
return
if (path.startswith(('/api/companion/', '/api/replay/', '/api/recordings/', '/api/connect/', '/api/cameras/', '/api/settings/', '/api/maps/',
'/api/bluetooth/',
'/api/starport/', '/api/spotify/', '/api/controllers/', '/api/vehicle-selection/', '/api/toyota-tools/',
'/api/models/', '/api/ui/', '/api/favorites/', '/api/sounds/', '/api/software/',
'/api/drives/', '/api/navigation/', '/api/drive-state/', '/api/sentry/', '/api/crash-reports/')) and
not self.require_session()):
return
if self.headers.get('Transfer-Encoding') is not None or \
self.headers.get('Content-Type', '').lower() != 'application/json':
self.json(415, {'error': 'JSON required'})
return
try:
size = int(self.headers.get('Content-Length', ''))
except ValueError:
size = -1
max_size = 256 * 1024 if path == '/api/settings/restore' else 17408 if path in ('/api/ui/layout', '/api/ui/layout/preview') else 4096
if not 0 < size <= max_size:
self.json(413, {'error': 'Invalid request size'})
return
try:
def unique_object(pairs):
result = {}
for name, value in pairs:
if name in result:
raise ValueError('Duplicate request field')
result[name] = value
return result
payload = json.loads(self.rfile.read(size), object_pairs_hook=unique_object)
except (ValueError, UnicodeError, RecursionError):
self.json(400, {'error': 'Invalid request'})
return
if path.startswith('/api/replay/'):
replay_api.post(self, path, payload)
return
if path == '/api/companion/location':
if getattr(self.server, 'remote_transport', False) or not self.direct_local():
self.json(403, {'error': 'Phone location requires a direct local connection'})
return
try:
self.json(200, phone_location_source.publish(payload))
except ValueError as error:
self.json(400, {'error': str(error)})
return
if path in ('/api/companion/uplink', '/api/companion/settings'):
if getattr(self.server, 'remote_transport', False) or not self.direct_local():
self.json(403, {'error': 'Companion controls require a direct local connection'})
return
try:
result = (companion_source.heartbeat(payload, self.client_address[0]) if path.endswith('/uplink')
else companion_source.save(payload))
except PermissionError as error:
self.json(403, {'error': str(error)})
except FileExistsError as error:
self.json(409, {'error': str(error)})
except ValueError as error:
self.json(400, {'error': str(error)})
except OSError:
self.json(503, {'error': 'Companion settings unavailable'})
else:
self.json(200, result)
return
if path == '/api/galaxy/hotspot':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
result = hotspot_owner().save(payload, lambda: self.settings_session() == identity and parked())
except PermissionError:
self.json(409, {'error': 'Park before changing the Galaxy hotspot.'})
except FileExistsError as error:
self.json(409, {'error': str(error)})
except ValueError as error:
self.json(400, {'error': str(error)})
except OSError:
self.json(503, {'error': 'Hotspot settings could not be saved.'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/recordings/action':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with recordings_lock:
result = recording_library.action(payload, permitted=lambda: self.settings_session() == identity and parked(),
before_delete=lambda route_id: replay_api.clear_route(self, identity, route_id))
except ReplayHTTPError as error:
self.json(error.status, {'error': error.message})
except ValueError as error:
self.json(400, {'error': str(error)})
except PermissionError as error:
self.json(409, {'error': str(error)})
except (OSError, DriveHistoryUnavailable):
self.json(503, {'error': 'Recording action could not be completed'})
else:
if self.require_session():
self.json(200, result)
return
if path == '/api/sentry/notifications':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if payload.get('action') == 'simulate':
with sentry_events_lock:
if set(payload) != {'action'}:
raise ValueError('Invalid simulation request')
if not parked():
self.json(403, {'error': 'Park the car before simulating an event'})
return
if clock() - last_simulation[0] < 10:
self.json(429, {'error': 'Wait ten seconds before simulating another event'})
return
notification_owner.store.record('warning', time.monotonic_ns(), simulated=True,
permitted=lambda: self.settings_session() == identity and parked())
last_simulation[0] = clock()
result = notification_owner.snapshot()
else:
result = notification_owner.action(payload, permitted=lambda: self.settings_session() == identity)
except LookupError:
self.json(404, {'error': 'Phone subscription not found'})
except ValueError as error:
self.json(400, {'error': str(error)})
except PermissionError:
self.json(401, {'error': 'Sign in to Galaxy'})
except (NotificationUnavailable, OSError):
self.json(503, {'error': 'Sentry notifications are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/connect/proxy':
identity = self.settings_session()
try:
def authorized():
return (identity is not None and self.settings_session() == identity and cloud_allowed() and
(not payload.get('enabled') or access.status().status == AccessStatus.CONFIGURED_LOCAL))
result = proxy_settings.save(payload, authorized)
except PermissionError as error:
self.json(403, {'error': str(error)})
except (ValueError, TypeError) as error:
self.json(409, {'error': str(error)})
except OSError:
self.json(503, {'error': 'Reverse proxy settings could not be saved'})
else:
self.json(200, {**result, 'canEdit': cloud_allowed(),
'passwordConfigured': access.status().status == AccessStatus.CONFIGURED_LOCAL})
return
if path == '/api/connect/provider':
identity = self.settings_session()
try:
if (type(payload) is not dict or set(payload) != {'provider', 'revision', 'confirmed'} or
type(payload['provider']) is not str or type(payload['revision']) is not str or payload['confirmed'] is not True):
raise ValueError('Confirm the next-boot cloud provider')
with effect_lock:
def authorized():
return identity is not None and self.settings_session() == identity and cloud_allowed()
if not authorized():
self.json(403, {'error': 'Turn off the vehicle before changing cloud providers'})
return
result = cloud_select(payload['provider'], payload['revision'], authorized)
except ValueError as error:
self.json(409, {'error': str(error)})
except (OSError, TypeError):
self.json(503, {'error': 'The cloud provider selection could not be saved'})
else:
if self.settings_session() == identity:
self.json(200, {**result, 'canSelect': cloud_allowed()})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/navigation/offline':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
result = offline_maps_owner().action(payload)
except ValueError as error:
self.json(400, {'error': str(error)})
except OSError:
self.json(503, {'error': 'Offline maps could not be saved. Try again shortly.'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path in ('/api/navigation/search', '/api/navigation/action'):
from openpilot.starpilot.navigation.owner import ConflictError, ValidationError
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if type(payload) is not dict:
raise ValidationError('Invalid navigation request')
if path == '/api/navigation/search':
if set(payload) not in ({'query'}, {'query', 'searchId', 'clientId'}, {'query', 'searchId', 'clientId', 'autocomplete'}):
raise ValidationError('Enter a destination to search')
if 'autocomplete' in payload and payload['autocomplete'] is not True:
raise ValidationError('Enter a destination to search')
result = {'results': (navigation_owner().search_places(payload['query'], identity, payload['searchId'], payload['clientId'],
autocomplete='autocomplete' in payload)
if 'searchId' in payload else navigation_owner().search(payload['query']))}
else:
fields = {'configure': {'patch'}, 'select': {'destination'}, 'selectPlace': {'id', 'searchId'},
'cancelSearch': {'searchId'}, 'clear': set(), 'favorite': {'destination'},
'favoritePlace': {'id', 'searchId'}, 'removeFavorite': {'id'}, 'labelFavorite': {'id', 'label'},
'removeRecent': {'id'}, 'clearRecents': set(), 'selectRoute': {'index'}}
action = payload.get('action')
optional = {'label'} if action in ('favorite', 'favoritePlace') and 'label' in payload else set()
if type(action) is not str or action not in fields or set(payload) != {'action', 'revision'} | fields[action] | optional:
raise ValidationError('Invalid navigation action')
needs_park = action == 'configure' and isinstance(payload['patch'], dict) and 'token' in payload['patch']
def authorized():
return self.settings_session() == identity and (not needs_park or configuration_allowed())
with (nullcontext() if action in ('selectPlace', 'favoritePlace') else effect_lock):
if not authorized():
raise PermissionError('Park your car before changing the Mapbox key' if needs_park else 'Sign in to Galaxy')
nav = navigation_owner()
keywords = {'expected_revision': payload['revision'], 'authorized': authorized}
if action == 'configure':
result = nav.configure(payload['patch'], **keywords)
elif action == 'selectPlace':
result = nav.select_place(payload['id'], payload['searchId'], identity, **keywords)
elif action == 'cancelSearch':
nav.cancel_search(identity, payload['searchId'])
result = nav.snapshot()
elif action == 'select':
result = nav.select(payload['destination'], **keywords)
elif action == 'clear':
result = nav.clear(**keywords)
elif action == 'selectRoute':
result = nav.select_route(payload['index'], **keywords)
elif action == 'favorite':
result = nav.favorite(payload['destination'], **keywords, label=payload.get('label'))
elif action == 'favoritePlace':
result = nav.favorite_place(payload['id'], payload['searchId'], identity, **keywords, label=payload.get('label'))
elif action == 'clearRecents':
result = nav.clear_recents(**keywords)
else:
if type(payload['id']) is not str or not 1 <= len(payload['id']) <= 256:
raise ValidationError('Choose a saved place')
if action == 'removeRecent':
result = nav.remove_recent(payload['id'], **keywords)
elif action == 'labelFavorite':
result = nav.label_favorite(payload['id'], payload['label'], **keywords)
else:
result = nav.remove_favorite(payload['id'], **keywords)
except ConflictError as error:
self.json(409, {'error': str(error)})
except ValidationError as error:
self.json(400, {'error': str(error)})
except PermissionError as error:
if self.require_session():
self.json(409, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'Navigation is unavailable. Try again shortly.'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/cameras/snapshot':
if type(payload) is not dict or set(payload) != {'camera'} or type(payload['camera']) is not str:
self.json(400, {'error': 'Choose a camera'})
return
identity = self.settings_session()
def permitted():
readable, _, _ = select.select([self.connection], [], [], 0)
if readable and not self.connection.recv(1, socket.MSG_PEEK | socket.MSG_DONTWAIT):
return False
return identity is not None and self.settings_session() == identity and parked()
try:
body = snapshots.capture(payload['camera'], permitted=permitted)
if not permitted():
raise SnapshotDenied
except ValueError:
self.json(400, {'error': 'Choose a supported camera'})
except SnapshotDenied:
if self.require_session():
self.json(409, {'error': 'A fresh parked vehicle connection is required'})
except SnapshotUnavailable:
if self.require_session():
self.json(503, {'error': 'No fresh camera frame. Try taking a new snapshot.'})
else:
self.respond(200, body, 'image/jpeg')
return
if path == '/api/galaxy/device-name':
if not self.require_session():
return
identity = self.settings_session()
try:
if type(payload) is not dict or set(payload) != {'name'}:
raise ValueError('Invalid comma name')
with effect_lock:
if identity is None or self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
result = {'name': device_name.save(payload['name'])}
except (ValueError, UnicodeError):
self.json(400, {'error': 'Use a name of up to 40 characters without control characters'})
except OSError:
self.json(503, {'error': 'The comma name could not be saved'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path in ('/api/galaxy/pair', '/api/galaxy/unpair'):
if getattr(self.server, 'remote_transport', False) or not self.direct_local():
self.json(403, {'error': 'Pair from the local device network'})
return
if not self.require_session():
return
if not parked():
self.json(409, {'error': 'Turn off the vehicle before changing Galaxy pairing'})
return
with effect_lock:
if path == '/api/galaxy/pair':
if pairing.read() is not None:
self.json(409, {'error': 'Galaxy is already paired'})
return
raw_password = payload.get('password') if type(payload) is dict and set(payload) == {'password'} else None
password = raw_password.strip() if type(raw_password) is str else None
if password is None or not 6 <= len(password) <= 255:
self.json(400, {'error': 'Password must be at least 6 characters'})
return
status = access.status().status
try:
previous_access = access.snapshot_for_pairing()
except (OSError, ValueError, UnicodeError):
self.json(503, {'error': 'Remote password storage is unavailable'})
return
legacy_root = access.legacy_root if status == AccessStatus.LEGACY_IMPORT_AVAILABLE else None
if status == AccessStatus.UNCONFIGURED:
if len(password) < 8:
self.json(400, {'error': 'New Galaxy passwords must be at least 8 characters'})
return
if not access.configure(password, parked):
self.json(503, {'error': 'Could not configure remote password'})
return
elif status == AccessStatus.LEGACY_IMPORT_AVAILABLE:
if not access.import_legacy(password, parked):
self.json(403, {'error': 'Galaxy password does not match existing pairing'})
return
elif status == AccessStatus.CONFIGURED_LOCAL:
if access.legacy_available() is None:
self.json(503, {'error': 'Existing Galaxy pairing storage is unavailable'})
return
if access.import_legacy(password, parked):
legacy_root = access.legacy_root
else:
if len(password) < 8:
self.json(400, {'error': 'New Galaxy passwords must be at least 8 characters'})
return
if not access.replace_for_pairing(password, parked):
self.json(503, {'error': 'Could not configure remote password'})
return
else:
self.json(503, {'error': 'Remote password storage is unavailable'})
return
if not parked():
generation = access.current_generation()
restored = generation is not None and access.restore_failed_pairing(previous_access, generation)
self.json(409 if restored else 503, {'error': 'Turn off the vehicle before changing Galaxy pairing' if restored else
'Galaxy pairing stopped; remote password storage needs review'})
return
try:
slug = pairing.pair(hashlib.sha256(password.encode()).hexdigest(), legacy_root=legacy_root)
except (OSError, ValueError):
slug = None
if slug is None:
generation = access.current_generation()
restored = generation is not None and access.restore_failed_pairing(previous_access, generation)
self.json(409 if restored else 503, {'error': 'Galaxy pairing storage is unavailable' if restored else
'Galaxy pairing could not be saved; remote password storage needs review'})
return
self.json(200, {'paired': True, 'url': pairing.url(slug)})
else:
if payload != {}:
self.json(400, {'error': 'Invalid request'})
return
if not parked():
self.json(409, {'error': 'Turn off the vehicle before changing Galaxy pairing'})
return
if not pairing.unpair():
self.json(409, {'error': 'Galaxy is not paired'})
return
self.json(200, {'paired': False})
return
if path == '/api/drives/ignore':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if (type(payload) is not dict or set(payload) != {'routeId', 'ignored'} or
type(payload['routeId']) is not str or len(payload['routeId']) > 180 or type(payload['ignored']) is not bool):
raise ValueError('Invalid drive selection')
with effect_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
result = statistics_owner().ignore(payload['routeId'], payload['ignored'],
authorized=lambda: self.settings_session() == identity)
except PermissionError:
self.json(401, {'error': 'Sign in to Galaxy'})
except ValueError:
if self.settings_session() == identity:
self.json(400, {'error': 'Invalid drive selection'})
except (OSError, RuntimeError):
if self.settings_session() == identity:
self.json(503, {'error': 'Driving history could not be saved'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path in ('/api/sentry/events/delete', '/api/crash-reports/delete'):
delete_records(self, path, payload, sentry_events_lock, crash_reports_lock, notification_owner, motion_events, reports)
elif path == '/api/recordings/delete-videos':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if type(payload) is not dict or set(payload) != {'segmentName'} or type(payload['segmentName']) is not str:
raise ValueError('Invalid segment selection')
with recordings_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if not parked():
self.json(409, {'error': 'Turn off the vehicle before managing recordings'})
return
replay_api.clear_route(self, identity, payload['segmentName'].rsplit('--', 1)[0].replace('_', '|'))
result = history.delete_videos(payload['segmentName'])
except ReplayHTTPError as error:
self.json(error.status, {'error': error.message})
except ValueError:
self.json(400, {'error': 'Invalid segment selection'})
except SegmentMissing:
self.json(404, {'error': 'Recording no longer exists; refresh the inventory'})
except SegmentInUse:
self.json(409, {'error': 'This segment is still recording'})
except (OSError, AttributeError):
self.json(503, {'error': 'Videos could not be deleted'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
elif path == '/api/software/action':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if type(payload) is not dict or type(payload.get('action')) is not str:
raise ValueError('Invalid software request')
with effect_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
result = software_owner().action(payload['action'], payload,
authorized=lambda: self.settings_session() == identity)
result = {**(software_source if software_source is not None else SoftwareStatus()).snapshot(), 'operations': result}
except SoftwareOperationError as error:
if self.settings_session() == identity:
self.json(error.status, {'error': str(error)})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
except ValueError as error:
self.json(400, {'error': str(error)})
except (SoftwareUnavailable, OSError, RuntimeError):
self.json(503, {'error': 'Software updates are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/favorites/slots':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
result = favorites_owner().save(payload, session_valid=lambda: self.settings_session() == identity)
except FavoritesChanged as error:
self.json(409, {'error': str(error)})
except ValueError as error:
self.json(400, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'Saved Favorites are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/starport/layout':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
result = projection_layout_owner().save(payload, session_valid=lambda: self.settings_session() == identity)
if result['document']['widgets'].get('nav_map', {}).get('enabled') is True:
try:
result['mapDisabledElsewhere'] = bool(layout_owner().disable_map(lambda: self.settings_session() == identity))
except (OSError, RuntimeError, ValueError, LayoutChanged):
result['mapDisabledElsewhere'] = False
except LayoutChanged as error:
self.json(409, {'error': str(error)})
except ValueError as error:
self.json(400, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'StarPort layout could not be saved'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/ui/layout':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
result = layout_owner().save(payload, session_valid=lambda: self.settings_session() == identity)
if result['document']['layouts']['large'].get('nav_map', {}).get('enabled') is True:
try:
result['mapDisabledElsewhere'] = bool(projection_layout_owner().disable_map(session_valid=lambda: self.settings_session() == identity))
except (OSError, RuntimeError, ValueError, LayoutChanged):
result['mapDisabledElsewhere'] = False
except LayoutChanged as error:
self.json(409, {'error': str(error)})
except ValueError as error:
self.json(400, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'Saved layout is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/ui/layout/preview':
preview_layout(self, payload, projection_layout_owner, layout_owner, layout_preview_socket)
return
if path == '/api/drive-state/action':
change_drive_state(self, payload, effect_lock, drive_owner)
return
if path.startswith('/api/sounds/'):
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
result = sound_owner().action(path.rsplit('/', 1)[-1], payload)
except SoundDownloadError as error:
self.json(error.status, {'error': str(error)})
except (OSError, ValueError, RuntimeError):
self.json(503, {'error': 'Sound packs are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path.startswith('/api/models/'):
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path.startswith('/api/models/laboratory'):
action = 'configure' if path.endswith('/laboratory') else path.rsplit('/', 1)[-1]
result = model_laboratory().action(action, payload)
else:
result = model_owner().action(path.rsplit('/', 1)[-1], payload)
except ValueError as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'Model management is unavailable'})
else:
self.json(200, result)
return
if path in ('/api/auth/login', '/api/auth/logout') and getattr(self.server, 'remote_transport', False):
self.json(409, {'error': 'Sign in through Galaxy', 'code': 'gateway_auth_required'})
return
if path == '/api/auth/logout':
if payload != {}:
self.json(400, {'error': 'Invalid request'})
return
with effect_lock:
identity = self.settings_session()
with session_lock:
token = self.session_token()
sessions.revoke(token)
if identity is not None and bluetooth_source is not None:
bluetooth_source.cancel_session(identity)
if identity is not None and starport_registry_source is not None:
starport_registry_source.revoke(identity)
self.respond(200, b'{"authenticated":false}', cookie='galaxy_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0')
return
if path.startswith('/api/spotify/'):
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
from openpilot.starpilot.galaxy.spotify_setup import SetupRejected as SpotifyRejected
try:
owner = spotify_owner()
if path == '/api/spotify/enable' and type(payload) is dict and set(payload) == {'enabled'}:
result = owner.enable(identity, payload['enabled'])
elif path == '/api/spotify/key' and type(payload) is dict and set(payload) == {'key'}:
result = owner.set_key(identity, payload['key'])
elif path == '/api/spotify/preferences' and type(payload) is dict:
result = owner.save_preferences(identity, payload)
elif path == '/api/spotify/control' and type(payload) is dict and set(payload) in ({'action'}, {'action', 'args'}):
result = owner.control(identity, payload['action'], payload.get('args', {}))
elif path == '/api/spotify/maintenance' and type(payload) is dict and set(payload) == {'action'}:
result = owner.maintenance(identity, payload['action'])
else:
self.json(400, {'error': 'Invalid Spotify request'})
return
except SpotifyRejected as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'Spotify is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/starport/enable':
if type(payload) is not dict or set(payload) not in ({'enabled'}, {'enabled', 'enableBluetooth'}) or \
type(payload['enabled']) is not bool or type(payload.get('enableBluetooth', False)) is not bool or \
payload.get('enableBluetooth', False) and not payload['enabled']:
self.json(400, {'error': 'Invalid Android Auto enable request'})
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
from openpilot.starpilot.galaxy.starport_setup import SetupRejected
try:
result = starport_setup_owner().enable(identity, payload['enabled'], enable_bluetooth=payload.get('enableBluetooth', False))
except SetupRejected as error:
self.json(409, {'error': str(error)})
except (BluetoothRejected, BluetoothUnavailable) as error:
self.json(409 if isinstance(error, BluetoothRejected) else 503,
{'error': 'Bluetooth could not be enabled; StarPort was not turned on', 'code': error.code})
except (OSError, RuntimeError):
self.json(503, {'error': 'Android Auto setting is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/starport/control':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if type(payload) is not dict or not isinstance(payload.get('action'), str):
self.json(400, {'error': 'Invalid Android Auto control request'})
return
action = payload['action']
if action == 'stop' and set(payload) == {'action'}:
try:
self.starport_command(identity, 'stop')
current = starport_client_owner().call('status').get('status', {})
except ValueError as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'Android Auto service is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, {'status': current})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if not starport_setup_owner().enabled():
self.json(409, {'error': 'Enable StarPort first'})
return
if action == 'start' and set(payload) == {'action'}:
command, arguments = 'start', {}
elif action == 'auto_connect' and set(payload) == {'action', 'enabled'} and type(payload['enabled']) is bool:
if not configuration_allowed():
self.json(403, {'error': 'Park before changing automatic connection'})
return
command, arguments = 'set_auto_connect', {'enabled': payload['enabled']}
elif (action == 'connection' and set(payload) == {'action', 'connection'} and
payload['connection'] in ('wireless', 'wired', 'headunitpad', 'openheadunit')):
if not configuration_allowed():
self.json(403, {'error': 'Park before changing the connection type'})
return
command, arguments = 'set_connection', {'connection': payload['connection']}
elif (action == 'car_type' and set(payload) == {'action', 'carType'} and
payload['carType'] in ('builtin', 'adapter', 'usb', 'app')):
if not configuration_allowed():
self.json(403, {'error': 'Park before changing the setup type'})
return
command, arguments = 'set_car_type', {'car_type': payload['carType']}
elif action == 'car_profile' and set(payload) == {'action', 'profile'} and isinstance(payload['profile'], str):
from openpilot.starpilot.system.starport.car_profiles import valid_override
if not valid_override(payload['profile']):
self.json(400, {'error': 'Invalid car profile'})
return
if not configuration_allowed():
self.json(403, {'error': 'Park before changing car settings'})
return
command, arguments = 'set_car_profile', {'profile': payload['profile']}
elif action == 'select_receiver' and set(payload) == {'action', 'address'} and isinstance(payload['address'], str):
if not configuration_allowed():
self.json(403, {'error': 'Park before changing the car'})
return
try:
address = normalized_address(payload['address'])
except ValueError:
self.json(400, {'error': 'Invalid car address'})
return
command, arguments = 'select_receiver', {'address': address}
else:
self.json(400, {'error': 'Invalid Android Auto control request'})
return
try:
runtime = starport_client_owner().call('status')['status']
if runtime.get('pairing_ready'):
raise ValueError('Finish or cancel pairing first')
if command == 'start' and not runtime.get('receiver_address') and runtime.get('connection') == 'wireless':
raise ValueError('Select a car first')
if command == 'start' and not starport_setup_owner().identity_status().get('installed'):
raise ValueError('Verify your Android Auto package first')
self.starport_command(identity, command, **arguments)
current = starport_client_owner().call('status')['status']
except ValueError as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError, KeyError, TypeError):
self.json(503, {'error': 'Android Auto control is unavailable'})
else:
if self.settings_session() == identity:
self.json(200, {'status': current})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/starport/pairing':
if payload != {}:
self.json(400, {'error': 'Invalid Android Auto pairing request'})
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if not configuration_allowed():
self.json(403, {'error': 'Park before pairing the car'})
return
if not starport_setup_owner().enabled():
self.json(403, {'error': 'Enable StarPort first'})
return
try:
setup_state = starport_setup_owner().status(identity)
except (OSError, RuntimeError):
self.json(503, {'error': 'StarPort setup is unavailable'})
return
if not setup_state['installReady'] or not setup_state['serviceReady']:
self.json(503, {'error': 'Android Auto display service is unavailable'})
return
if not setup_state['bluetoothEnabled']:
self.json(409, {'error': 'Turn on Bluetooth before pairing the car'})
return
# Bluetooth pairing and the Wi-Fi bootstrap need no identity; only projection does.
source = None
try:
source = starport_source_registry().mint(identity)
if self.settings_session() != identity or not configuration_allowed():
raise ValueError('Galaxy session or Park state changed')
starport_client_owner().call('prepare_pairing', source=source)
except ValueError as error:
if source is not None:
starport_source_registry().revoke_source(source)
self.json(409, {'error': str(error)})
except (OSError, RuntimeError):
if source is not None:
starport_source_registry().revoke_source(source)
self.json(503, {'error': 'StarPort pairing service is unavailable'})
else:
if self.settings_session() == identity and configuration_allowed():
self.json(200, {'pairing': True, 'seconds': 180})
else:
starport_source_registry().revoke_source(source)
self.json(409, {'error': 'Galaxy session or Park state changed'})
return
if path in ('/api/starport/pairing/response', '/api/starport/pairing/cancel', '/api/starport/pairing/select'):
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if not configuration_allowed():
self.json(403, {'error': 'Park before pairing the car'})
return
source = starport_source_registry().current(identity)
if source is None:
self.json(409, {'error': 'Pairing window expired'})
return
if path.endswith('/cancel'):
if payload != {}:
self.json(400, {'error': 'Invalid pairing cancellation'})
return
command, arguments = 'cancel_pairing', {}
elif path.endswith('/select'):
try:
if set(payload) != {'address'}:
raise ValueError('Invalid car selection')
address = normalized_address(payload['address'])
except (TypeError, ValueError):
self.json(400, {'error': 'Invalid car address'})
return
command, arguments = 'pair_device', {'address': address}
else:
if (set(payload) != {'prompt_id', 'accepted', 'value'} or
not isinstance(payload['prompt_id'], str) or len(payload['prompt_id']) != 32 or
type(payload['accepted']) is not bool or not isinstance(payload['value'], str) or len(payload['value']) > 16):
self.json(400, {'error': 'Invalid pairing response'})
return
command, arguments = 'pairing_response', payload
try:
if self.settings_session() != identity or not configuration_allowed():
raise ValueError('Galaxy session or Park state changed')
starport_client_owner().call(command, source=source, **arguments)
except ValueError as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError):
self.json(409, {'error': 'Car connection could not start. Refresh the list and try again.' if command == 'pair_device' else
'Pairing prompt expired or changed'})
else:
if command == 'cancel_pairing':
starport_source_registry().revoke_source(source)
self.json(200, {'ok': True})
return
if path == '/api/toyota-tools/action':
from openpilot.starpilot.galaxy.toyota_tools import ToyotaToolsChanged
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
result = toyota_owner().action(payload, *identity, session_valid=lambda: self.settings_session() == identity)
except ValueError:
self.json(400, {'error': 'Invalid Toyota request'})
except ToyotaToolsChanged:
self.json(409, {'error': 'Toyota settings changed; refresh saved values'})
except (OSError, RuntimeError):
self.json(503, {'error': 'Toyota settings are unavailable'})
else:
self.json(200, result) if self.settings_session() == identity else self.json(401, {'error': 'Sign in to Galaxy'})
return
if path.startswith('/api/vehicle-selection/'):
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if path.endswith('/preview'):
if type(payload) is not dict or set(payload) != {'view', 'platform'} or \
type(payload['view']) is not str or not 1 <= len(payload['view']) <= 64 or \
(payload['platform'] is not None and type(payload['platform']) is not str):
raise ValueError('Invalid vehicle request')
result = vehicle_choices().preview(payload['view'], payload['platform'], *identity)
else:
if type(payload) is not dict or set(payload) != {'intent', 'confirmed'} or \
type(payload['intent']) is not str or not 1 <= len(payload['intent']) <= 64 or payload['confirmed'] is not True:
raise ValueError('Invalid vehicle request')
with effect_lock:
if self.settings_session() != identity:
raise VehicleSelectionChanged('Session changed')
saved = vehicle_choices().confirm(payload['intent'], *identity,
session_valid=lambda: self.settings_session() == identity)
result = {'saved': saved}
except ValueError:
if self.settings_session() == identity:
self.json(400, {'error': 'Invalid vehicle request'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
except VehicleSelectionChanged:
if self.settings_session() == identity:
self.json(409, {'error': 'Vehicle selection changed; refresh the page'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
except VehicleSelectionUnverified:
if self.settings_session() == identity:
self.json(409, {'error': 'Save could not be confirmed; refresh saved values', 'code': 'unverified'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
except (OSError, RuntimeError):
if self.settings_session() == identity:
self.json(503, {'error': 'Vehicle selection is unavailable'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/bluetooth/action':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if type(payload) is not dict or set(payload) not in ({'operation'}, {'operation', 'address'}, {'operation', 'enabled'},
{'operation', 'promptId', 'accepted'},
{'operation', 'promptId', 'accepted', 'value'}) or \
type(payload.get('operation')) is not str or payload['operation'] not in BluetoothOwner.OPERATIONS:
raise ValueError('Invalid Bluetooth request')
if payload['operation'] == 'power':
if set(payload) != {'operation', 'enabled'} or type(payload['enabled']) is not bool:
raise ValueError('Invalid Bluetooth request')
elif payload['operation'] in ('scan', 'stop_scan', 'cancel_pair', 'accept_pair'):
if set(payload) != {'operation'}:
raise ValueError('Invalid Bluetooth request')
elif payload['operation'] == 'pairing_response':
if (set(payload) not in ({'operation', 'promptId', 'accepted'}, {'operation', 'promptId', 'accepted', 'value'}) or
type(payload['accepted']) is not bool or type(payload['promptId']) is not str or
not re.fullmatch('[0-9a-f]{32}', payload['promptId']) or
type(payload.get('value', '')) is not str or len(payload.get('value', '')) > 16):
raise ValueError('Invalid Bluetooth request')
elif set(payload) != {'operation', 'address'}:
raise ValueError('Invalid Bluetooth request')
else:
normalized_address(payload['address'])
with effect_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
delegated = {'handled': False}
if payload['operation'] == 'accept_pair' and configuration_allowed():
spotify_owner().prepare_phone_pairing()
if payload['operation'] in ('connect', 'disconnect', 'forget') and starport_setup_owner().enabled():
try:
delegated = self.starport_command(identity, 'bluetooth_action', operation=payload['operation'],
address=normalized_address(payload['address']))
except (OSError, RuntimeError):
# Without a running Android Auto service nothing else holds the adapter, so forgetting is local.
if payload['operation'] != 'forget':
raise
delegated = {'handled': False}
if type(delegated) is not dict or type(delegated.get('handled')) is not bool:
raise BluetoothUnavailable('service_unavailable')
if 'error_code' in delegated:
code = delegated['error_code']
if type(code) is str and code in ('busy', 'park_required', 'changed', 'session_expired', 'projection_active'):
raise BluetoothRejected('Android Auto could not complete the Bluetooth change', code=code)
unavailable = ('service_unavailable', 'adapter_unavailable', 'radio_unavailable', 'radio_preference_unavailable')
raise BluetoothUnavailable('Android Auto Bluetooth service is unavailable',
code=code if type(code) is str and code in unavailable else 'service_unavailable')
elif payload['operation'] == 'power' and starport_setup_owner().enabled():
# Android Auto may hold the adapter in its own process; it releases that hold before the radio stops.
try:
delegated = self.starport_command(identity, 'bluetooth_power', enabled=payload['enabled'])
except (OSError, RuntimeError):
delegated = {'handled': False}
if type(delegated) is not dict or type(delegated.get('handled')) is not bool:
delegated = {'handled': False}
if 'error_code' in delegated:
code = delegated['error_code']
if type(code) is str and code in ('busy', 'park_required', 'changed', 'session_expired', 'projection_active'):
why = delegated.get('message')
raise BluetoothRejected(f'Android Auto: {why}' if type(why) is str and why else
'Android Auto could not complete the Bluetooth change', code=code)
raise BluetoothUnavailable('Android Auto Bluetooth service is unavailable')
elif payload['operation'] in ('accept_pair', 'pair', 'scan', 'stop_scan', 'pairing_response', 'cancel_pair') and starport_setup_owner().enabled():
delegated = self.starport_phone_window(identity, payload['operation'], payload)
if 'error_code' in delegated:
code = delegated['error_code']
if type(code) is str and code in ('busy', 'park_required', 'changed', 'session_expired'):
why = delegated.get('message')
raise BluetoothRejected(f'Android Auto: {why}' if type(why) is str and why else
'Android Auto could not complete the Bluetooth change', code=code)
raise BluetoothUnavailable('Android Auto Bluetooth service is unavailable')
if delegated['handled']:
result = delegated.get('bluetooth')
if type(result) is not dict:
raise BluetoothUnavailable('service_unavailable')
else:
result = bluetooth_owner().request(payload['operation'], address=payload.get('address'), enabled=payload.get('enabled'),
session=identity, prompt_id=payload.get('promptId'),
accepted=payload.get('accepted'), value=payload.get('value', ''))
if payload['operation'] in ('accept_pair', 'cancel_pair'):
self.phone_claim(payload['operation'], result)
elif payload['operation'] == 'forget':
forget_app_keys(payload['address'])
except ValueError:
if self.settings_session() == identity:
self.json(400, {'error': 'Invalid Bluetooth request'})
except BluetoothRejected as error:
cloudlog.warning(f'bluetooth {payload.get("operation")} refused ({error.code}): {error}')
if self.settings_session() == identity:
self.json(409, {'error': 'Bluetooth change was not accepted', 'code': error.code})
except BluetoothUnavailable as error:
cloudlog.warning(f'bluetooth {payload.get("operation")} unavailable ({error.code}): {error}')
if self.settings_session() == identity:
self.json(503, {'error': 'Bluetooth service could not complete the request', 'code': error.code})
except (OSError, RuntimeError):
if self.settings_session() == identity:
self.json(503, {'error': 'Bluetooth service could not complete the request', 'code': 'service_unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path == '/api/controllers/action':
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
with effect_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if not layout_owner().parked():
self.json(403, {'error': 'Turn off the vehicle to change controller buttons'})
return
result = controller_action(payload, controllers_socket)
except ControllerInvalid:
self.json(400, {'error': 'Invalid controller request'})
except ControllerDenied:
self.json(403, {'error': 'Turn off the vehicle to change controller buttons'})
except ControllerBusy:
self.json(429, {'error': 'Controller is busy'})
except (ControllerUnavailable, OSError, RuntimeError):
self.json(503, {'error': 'Controller buttons are unavailable'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path.startswith('/api/maps/'):
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
operation = path.rsplit('/', 1)[1]
try:
validate_action(operation, payload)
# Serialize command dispatch with logout. The independent owner
# rechecks actual parked evidence throughout the accepted operation.
with effect_lock:
if self.settings_session() != identity:
self.json(401, {'error': 'Sign in to Galaxy'})
return
result = operations.request(operation, payload)
except ValueError:
if self.require_session():
self.json(400, {'error': 'Invalid map operation'})
except MapOperationError as error:
if self.settings_session() == identity:
self.json(error.status, {'error': 'Map operation was not accepted', 'code': error.code})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if path.startswith('/api/settings/'):
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
try:
if path == '/api/settings/restore':
from openpilot.starpilot.galaxy.toggle_backup import ToggleBackup
with effect_lock:
result = ToggleBackup(feature_settings(), layout_owner()).restore(
payload, identity, authorized=lambda: self.settings_session() == identity)
elif path == '/api/settings/recovery/preview':
result = preview_settings_recovery(payload, settings_recovery, identity)
elif path == '/api/settings/recovery/confirm':
if (not isinstance(payload, dict) or set(payload) != {'intent', 'confirmed'} or
not isinstance(payload['intent'], str) or len(payload['intent']) > 64 or payload['confirmed'] is not True):
raise ValueError('Invalid settings recovery confirmation')
with effect_lock:
if self.settings_session() != identity:
raise SettingsChanged('Session changed')
result = settings_recovery().confirm(payload['intent'], *identity,
session_valid=lambda: self.settings_session() == identity)
elif path == '/api/settings/reset-default':
if not isinstance(payload, dict) or set(payload) != {'view', 'row'} or \
not isinstance(payload['view'], str) or len(payload['view']) > 64 or type(payload['row']) is not int:
raise ValueError('Invalid default reset request')
result = feature_settings().preview(payload['view'], payload['row'], 0, *identity, reset_default=True)
elif path == '/api/settings/preview':
if not isinstance(payload, dict) or set(payload) not in ({'view', 'row', 'direction'},
{'view', 'row', 'direction', 'draft'}, {'view', 'row', 'value'}) or \
not isinstance(payload['view'], str) or len(payload['view']) > 64 or \
type(payload['row']) is not int or \
('direction' in payload and (type(payload['direction']) is not int or payload['direction'] not in (-1, 0, 1))) or \
('value' in payload and type(payload['value']) not in (str, int, float)) or \
('draft' in payload and type(payload['draft']) is not dict):
raise ValueError('Invalid preview request')
result = feature_settings().preview(payload['view'], payload['row'], payload.get('direction', 0), *identity,
draft=payload.get('draft'), value=payload.get('value'))
else:
if not isinstance(payload, dict) or set(payload) not in ({'intent', 'confirmed'}, {'intent', 'confirmed', 'page'}) or \
not isinstance(payload['intent'], str) or len(payload['intent']) > 64 or payload['confirmed'] is not True:
raise ValueError('Invalid confirmation request')
if 'page' in payload and (type(payload['page']) is not str or payload['page'] not in PAGES):
raise ValueError('Invalid settings page')
with effect_lock:
if self.settings_session() != identity:
raise SettingsChanged('Session changed')
saved = feature_settings().confirm(payload['intent'], *identity,
session_valid=lambda: self.settings_session() == identity)
if not saved:
raise SettingsChanged('Saved source or vehicle state changed')
result = {'saved': True}
if 'page' in payload:
try:
result['page'] = feature_settings().page(payload['page'], *identity)
except (SettingsChanged, SettingsUnavailable, OSError, RuntimeError):
pass
except ValueError:
if self.settings_session() == identity:
self.json(400, {'error': 'Invalid request'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
except SettingsChanged:
if self.settings_session() == identity:
self.json(409, {'error': 'Saved settings changed; refresh the page'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
except (SettingsUnavailable, OSError, RuntimeError):
if self.settings_session() == identity:
self.json(503, {'error': 'Saved settings are unavailable'})
else:
self.json(401, {'error': 'Sign in to Galaxy'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if not isinstance(payload, dict) or set(payload) != {'password'} or \
not isinstance(payload['password'], str) or not 6 <= len(payload['password']) <= 255:
self.json(400, {'error': 'Invalid request'})
return
status, result, token = login(payload['password'])
cookie = None if token is None else f'galaxy_session={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=1800'
self.respond(status, json.dumps(result).encode(), cookie=cookie)
def do_rejected_method(self):
if self.local_request(mutation=True):
self.json(405, {'error': 'Method unavailable'})
def do_DELETE(self):
if not self.local_request(mutation=True):
return
if urlsplit(self.path).path == '/api/spotify':
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
from openpilot.starpilot.galaxy.spotify_setup import SetupRejected as SpotifyRejected
try:
result = spotify_owner().remove(identity)
except SpotifyRejected as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError):
self.json(503, {'error': 'Spotify data could not be removed'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
return
if urlsplit(self.path).path != '/api/starport/identity':
self.json(405, {'error': 'Method unavailable'})
return
if not self.require_session():
return
identity = self.settings_session()
if identity is None:
self.json(401, {'error': 'Sign in to Galaxy'})
return
from openpilot.starpilot.galaxy.starport_setup import SetupRejected
def disconnect():
if self.settings_session() != identity:
raise SetupRejected('Galaxy session expired; sign in again')
client = starport_client_owner()
if client.available:
runtime = client.call('status')['status']
if not isinstance(runtime, dict) or type(runtime.get('running')) is not bool:
raise RuntimeError('Android Auto connection status is unavailable')
if runtime['running']:
if self.settings_session() != identity:
raise SetupRejected('Galaxy session expired; sign in again')
# Stop is permitted with Android Auto off; this DELETE already authenticated the caller.
client.call('stop')
current = client.call('status')['status']
if not isinstance(current, dict) or current.get('running') is not False:
raise SetupRejected('Android Auto is still disconnecting; try deleting the package again')
if self.settings_session() != identity:
raise SetupRejected('Galaxy session expired; sign in again')
try:
result = starport_setup_owner().remove(identity, disconnect=disconnect)
except SetupRejected as error:
self.json(409, {'error': str(error)})
except (OSError, RuntimeError, KeyError, TypeError):
self.json(503, {'error': 'Android Auto could not be disconnected or its package removed'})
else:
if self.settings_session() == identity:
self.json(200, result)
else:
self.json(401, {'error': 'Sign in to Galaxy'})
do_PUT = do_PATCH = do_OPTIONS = do_rejected_method
try:
server = _LocalHTTPServer((host, port), Handler)
if evidence_source is not None:
evidence_source.start()
if notification_owner is None:
notification_owner = NotificationOwner()
server.notification_source = notification_owner
notification_owner.start()
except BaseException:
if notification_owner is not None:
notification_owner.close()
if pairing_authority is not None:
pairing_authority.close()
if evidence_source is not None:
evidence_source.close()
if "server" in locals():
server.server_close(close_sources=False)
raise
if local_access is None:
address_source.port = server.server_port
server.device_state_source = vehicle_display
if pairing_authority is not None:
server.pairing_authority = pairing_authority
server.evidence_source = evidence_source
server.map_source = map_source
if recording_media_source is not None:
server.recording_media_source = recording_media_source
if model_source is not None:
server.model_source = model_source
if plot_source is not None:
server.plots_source = plot_source
if settings_gateway is not None:
server.settings_source = settings_gateway
if toyota_source is not None:
server.toyota_tools_source = toyota_source
if bluetooth_source is not None:
server.bluetooth_source = bluetooth_source
if starport_setup_source is not None:
server.starport_setup_source = starport_setup_source
if software_operations_source is not None:
server.software_operations_source = software_operations_source
if drive_stats_source is not None:
server.drive_stats_source = drive_stats_source
legacy_google_removed = False
def maintain_starport():
nonlocal legacy_google_removed
if not legacy_google_removed:
legacy_google_removed = True
from openpilot.starpilot.galaxy.starport_setup import remove_legacy_google_files
remove_legacy_google_files()
server.maintain_starport = maintain_starport
server.start_drive_history = lambda: statistics_owner().start()
return server
def make_remote_server(local_server, *, port=8084):
"""Share local owners and sessions while forcing all tunnel requests through password auth."""
server = _LocalHTTPServer(('127.0.0.1', port), local_server.RequestHandlerClass)
server.remote_transport = True
return server
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port', type=int, default=8082)
parser.add_argument('--host', default='127.0.0.1', choices=('127.0.0.1', '0.0.0.0'))
args = parser.parse_args()
with make_server(port=args.port, host=args.host) as server:
server.start_drive_history()
print(f'Galaxy listening on {args.host}:{server.server_port}', flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == '__main__':
main()