From 2017bf970f449abecb9f6b629b35ed6eebdac30a Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Tue, 10 Jun 2025 16:49:47 +0800 Subject: [PATCH] ui: implement ssh key control (#35518) implement ssh key control --- selfdrive/ui/layouts/settings/developer.py | 6 + selfdrive/ui/widgets/ssh_key.py | 127 +++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 selfdrive/ui/widgets/ssh_key.py diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index 101dd82623..607d96bcf7 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -1,6 +1,7 @@ from openpilot.system.ui.lib.application import Widget from openpilot.system.ui.lib.list_view import ListView, toggle_item from openpilot.common.params import Params +from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item # Description constants DESCRIPTIONS = { @@ -9,6 +10,10 @@ DESCRIPTIONS = { "See https://docs.comma.ai/how-to/connect-to-comma for more info." ), 'joystick_debug_mode': "Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)", + 'ssh_key': ( + "Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username " + + "other than your own. A comma employee will NEVER ask you to add their GitHub username." + ), } @@ -23,6 +28,7 @@ class DeveloperLayout(Widget): initial_state=self._params.get_bool("AdbEnabled"), callback=self._on_enable_adb, ), + ssh_key_item("SSH Key", description=DESCRIPTIONS["ssh_key"]), toggle_item( "Joystick Debug Mode", description=DESCRIPTIONS["joystick_debug_mode"], diff --git a/selfdrive/ui/widgets/ssh_key.py b/selfdrive/ui/widgets/ssh_key.py new file mode 100644 index 0000000000..e45b04e6ba --- /dev/null +++ b/selfdrive/ui/widgets/ssh_key.py @@ -0,0 +1,127 @@ +import pyray as rl +import requests +import threading +import copy +from enum import Enum + +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import gui_app, DialogResult, FontWeight +from openpilot.system.ui.lib.button import gui_button, ButtonStyle +from openpilot.system.ui.lib.list_view import ( + ItemAction, + ListItem, + BUTTON_HEIGHT, + BUTTON_BORDER_RADIUS, + BUTTON_FONT_SIZE, + BUTTON_WIDTH, +) +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets.confirm_dialog import alert_dialog +from openpilot.system.ui.widgets.keyboard import Keyboard + + +class SshKeyActionState(Enum): + LOADING = "LOADING" + ADD = "ADD" + REMOVE = "REMOVE" + + +class SshKeyAction(ItemAction): + HTTP_TIMEOUT = 15 # seconds + MAX_WIDTH = 500 + + def __init__(self): + super().__init__(self.MAX_WIDTH, True) + + self._keyboard = Keyboard() + self._params = Params() + self._error_message: str = "" + self._text_font = gui_app.font(FontWeight.MEDIUM) + + self._refresh_state() + + def _refresh_state(self): + self._username = self._params.get("GithubUsername", "") + self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD + + def _render(self, rect: rl.Rectangle) -> bool: + # Show error dialog if there's an error + if self._error_message: + message = copy.copy(self._error_message) + gui_app.set_modal_overlay(lambda: alert_dialog(message)) + self._username = "" + self._error_message = "" + + # Draw username if exists + if self._username: + text_size = measure_text_cached(self._text_font, self._username, BUTTON_FONT_SIZE) + rl.draw_text_ex( + self._text_font, + self._username, + (rect.x + rect.width - BUTTON_WIDTH - text_size.x - 30, rect.y + (rect.height - text_size.y) / 2), + BUTTON_FONT_SIZE, + 1.0, + rl.WHITE, + ) + + # Draw button + if gui_button( + rl.Rectangle( + rect.x + rect.width - BUTTON_WIDTH, rect.y + (rect.height - BUTTON_HEIGHT) / 2, BUTTON_WIDTH, BUTTON_HEIGHT + ), + self._state.value, + is_enabled=self._state != SshKeyActionState.LOADING, + border_radius=BUTTON_BORDER_RADIUS, + font_size=BUTTON_FONT_SIZE, + button_style=ButtonStyle.LIST_ACTION, + ): + self._handle_button_click() + return True + return False + + def _handle_button_click(self): + if self._state == SshKeyActionState.ADD: + self._keyboard.clear() + self._keyboard.set_title("Enter your GitHub username") + gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit) + elif self._state == SshKeyActionState.REMOVE: + self._params.remove("GithubUsername") + self._params.remove("GithubSshKeys") + self._refresh_state() + + def _on_username_submit(self, result: DialogResult): + if result != DialogResult.CONFIRM: + return + + username = self._keyboard.text.strip() + if not username: + return + + self._state = SshKeyActionState.LOADING + threading.Thread(target=lambda: self._fetch_ssh_key(username), daemon=True).start() + + def _fetch_ssh_key(self, username: str): + try: + url = f"https://github.com/{username}.keys" + response = requests.get(url, timeout=self.HTTP_TIMEOUT) + response.raise_for_status() + keys = response.text.strip() + if not keys: + raise requests.exceptions.HTTPError("No SSH keys found") + + # Success - save keys + self._params.put("GithubUsername", username) + self._params.put("GithubSshKeys", keys) + self._state = SshKeyActionState.REMOVE + self._username = username + + except requests.exceptions.Timeout: + self._error_message = "Request timed out" + self._state = SshKeyActionState.ADD + except Exception: + self._error_message = f"No SSH keys found for user '{username}'" + self._state = SshKeyActionState.ADD + + +def ssh_key_item(title: str, description: str): + return ListItem(title=title, description=description, action_item=SshKeyAction())