From 4e32845a5f711e5d93a055475c44896738ac7b3e Mon Sep 17 00:00:00 2001 From: Allison Karlitskaya Date: Sat, 6 Jul 2024 10:29:36 -0400 Subject: [PATCH] beiboot: split flatpak and bastion code paths These have diverged enough that splitting them out from each other would substantially improve readability. --- src/cockpit/beiboot.py | 129 +++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 44 deletions(-) diff --git a/src/cockpit/beiboot.py b/src/cockpit/beiboot.py index 4799fac2d6a..f4aa77f33d1 100644 --- a/src/cockpit/beiboot.py +++ b/src/cockpit/beiboot.py @@ -23,6 +23,7 @@ import os import re import shlex +import tempfile from pathlib import Path from typing import Dict, Iterable, Optional, Sequence @@ -33,7 +34,7 @@ from cockpit.bridge import setup_logging from cockpit.channel import ChannelRoutingRule from cockpit.channels import PackagesChannel -from cockpit.jsonutil import JsonObject +from cockpit.jsonutil import JsonObject, get_str from cockpit.packages import Packages, PackagesLoader, patch_libexecdir from cockpit.peer import Peer from cockpit.protocol import CockpitProblem @@ -203,6 +204,38 @@ async def do_custom_command(self, command: str, args: tuple, fds: list[int], std self.router.routing_rules.insert(0, ChannelRoutingRule(self.router, [PackagesChannel])) +def python_interpreter(comment: str) -> tuple[Sequence[str], Sequence[str]]: + return ('python3', '-ic', f'# {comment}'), () + + +def via_ssh(cmd: Sequence[str], dest: str, ssh_askpass: Path, *ssh_opts: str) -> tuple[Sequence[str], Sequence[str]]: + host, _, port = dest.rpartition(':') + # catch cases like `host:123` but not cases like `[2001:abcd::1] + if port.isdigit(): + destination = ['-p', port, host] + else: + destination = [dest] + + return ( + 'ssh', *ssh_opts, *destination, shlex.join(cmd) + ), ( + f'SSH_ASKPASS={ssh_askpass!s}', + # DISPLAY=x helps trigger a heuristic in old ssh versions to force them + # to use askpass. Newer ones look at SSH_ASKPASS_REQUIRE. + 'DISPLAY=x', + 'SSH_ASKPASS_REQUIRE=force', + ) + + +def flatpak_spawn(cmd: Sequence[str], env: Sequence[str]) -> tuple[Sequence[str], Sequence[str]]: + return ( + 'flatpak-spawn', '--host', + *(f'--env={kv}' for kv in env), + *cmd + ), ( + ) + + class SshPeer(Peer): always: bool @@ -212,60 +245,68 @@ def __init__(self, router: Router, destination: str, args: argparse.Namespace): super().__init__(router) async def do_connect_transport(self) -> None: - # do we have user/password (Basic auth) from the login page? - auth = await self.router.request_authorization("*") - ssh_opts = [] + # Choose your own adventure... + if os.path.exists('/.flatpak-info'): + await self.connect_from_flatpak() + else: + await self.connect_from_bastion_host() + + async def connect_from_flatpak(self) -> None: + # We want to run a python interpreter somewhere... + cmd, env = python_interpreter('cockpit-bridge') + + # Remote host? Wrap command with SSH + if self.destination != 'localhost': + # we run ssh and thus the helper on the host, always use the xdg-cache helper + cmd, env = via_ssh(cmd, self.destination, ensure_ferny_askpass()) + + cmd, env = flatpak_spawn(cmd, env) + + await self.boot(cmd, env) + + async def connect_from_bastion_host(self) -> None: basic_password = None + username_opts = [] + + # do we have user/password (Basic auth) from the login page? + auth = await self.router.request_authorization_object("*") + response = get_str(auth, 'response') + user_known_hosts = get_str(auth, 'user-known-hosts', '') - if auth is not None and auth.startswith('Basic '): - user, basic_password = base64.b64decode(auth.split(' ', 1)[1]).decode().split(':', 1) + if response.startswith('Basic '): + user, basic_password = base64.b64decode(response.split(' ', 1)[1]).decode().split(':', 1) if user: # this can be empty, i.e. auth is just ":" logger.debug("got username %s and password from Basic auth", user) - ssh_opts = ['-l', user] + username_opts = ['-l', user] - beiboot_helper = BridgeBeibootHelper(self) + # We want to run a python interpreter somewhere... + cmd, env = python_interpreter('cockpit-bridge') - agent = ferny.InteractionAgent([AuthorizeResponder(self.router, basic_password), beiboot_helper]) + # outside of the flatpak we expect cockpit-ws and thus an installed helper + askpass = patch_libexecdir('${libexecdir}/cockpit-askpass') + assert isinstance(askpass, str) + ssh_askpass = Path(askpass) + if not ssh_askpass.exists(): + logger.error("Could not find cockpit-askpass helper at %r", askpass) - # We want to run a python interpreter somewhere... - cmd: Sequence[str] = ('python3', '-ic', '# cockpit-bridge') - env: Sequence[str] = () + with tempfile.TemporaryDirectory() as tmpdir: + user_known_hosts_file = Path(tmpdir) / 'user-known-hosts' + user_known_hosts_file.write_text(user_known_hosts) - in_flatpak = os.path.exists('/.flatpak-info') + cmd, env = via_ssh(cmd, self.destination, ssh_askpass, *username_opts, + '-o', f'UserKnownHostsfile={user_known_hosts_file!s}') - # Remote host? Wrap command with SSH - if self.destination != 'localhost': - if in_flatpak: - # we run ssh and thus the helper on the host, always use the xdg-cache helper - ssh_askpass = ensure_ferny_askpass() - else: - # outside of the flatpak we expect cockpit-ws and thus an installed helper - askpass = patch_libexecdir('${libexecdir}/cockpit-askpass') - assert isinstance(askpass, str) - ssh_askpass = Path(askpass) - if not ssh_askpass.exists(): - logger.error("Could not find cockpit-askpass helper at %r", askpass) - - env = ( - f'SSH_ASKPASS={ssh_askpass!s}', - 'DISPLAY=x', - 'SSH_ASKPASS_REQUIRE=force', - ) - host, _, port = self.destination.rpartition(':') - # catch cases like `host:123` but not cases like `[2001:abcd::1] - if port.isdigit(): - ssh_opts += ['-p', port, host] - else: - ssh_opts += [self.destination] + await self.boot(cmd, env, basic_password) - cmd = ('ssh', *ssh_opts, shlex.join(cmd)) + self.router.write_control( + command='authorize', challenge='x-login-data', cookie='-', login_data={ + 'user-known-hosts': user_known_hosts_file.read_text() + } + ) - # Running in flatpak? Wrap command with flatpak-spawn --host - if in_flatpak: - cmd = ('flatpak-spawn', '--host', - *(f'--env={kv}' for kv in env), - *cmd) - env = () + async def boot(self, cmd: Sequence[str], env: Sequence[str], basic_password: str | None = None) -> None: + beiboot_helper = BridgeBeibootHelper(self) + agent = ferny.InteractionAgent([AuthorizeResponder(self.router, basic_password), beiboot_helper]) logger.debug("Launching command: cmd=%s env=%s", cmd, env) transport = await self.spawn(cmd, env, stderr=agent, start_new_session=True)