python: update ferny, implement cockpit.send-stderr

ferny stopped implementing our send-stderr command, which was only used
for us, so we have to implement it ourselves now, from our
InteractionResponder in superuser.py.

On the plus side, we can now use interaction_client.py to send custom
commands.  According to the ferny documentation, we should prefer this
over sending messages for ourselves.

Note: in comparison to the old send-stderr handling inside of ferny, our
new `cockpit.send-stderr` command no longer implies that the ferny
interaction is complete — we need to send `ferny.end` separately.  This
isn't actually required for the interaction to exit (since we'll send
our "init" message very soon anyway) but is required in some situations
to prevent ferny from interpreting our dropping the stderr socket as an
unexpected exit.
This commit is contained in:
Allison Karlitskaya 2023-05-04 15:29:59 +02:00 committed by Martin Pitt
parent 3ca979d542
commit 61973c8c2b
4 changed files with 28 additions and 17 deletions

@ -1 +1 @@
Subproject commit 39fe96ce1f5928d51e80759f35d70930bbb57ee1
Subproject commit 4d4319fa2e5c15010612544e95bc61636935dea6

View File

@ -28,6 +28,7 @@ import socket
import subprocess
from typing import Dict, Iterable, List, Tuple, Type
from cockpit._vendor.ferny import interaction_client
from cockpit._vendor.systemd_ctypes import bus, run_async
from . import polyfills
@ -150,24 +151,25 @@ async def run(args) -> None:
def try_to_receive_stderr():
fds = []
try:
stderr_socket = socket.fromfd(2, socket.AF_UNIX, socket.SOCK_STREAM)
ours, theirs = socket.socketpair()
socket.send_fds(stderr_socket, [b'\0ferny\0(["send-stderr"], {})'], [theirs.fileno(), 1])
theirs.close()
_msg, fds, _flags, _addr = socket.recv_fds(ours, 1, 1)
with ours:
with theirs:
interaction_client.command(2, 'cockpit.send-stderr', fds=[theirs.fileno()])
_msg, fds, _flags, _addr = socket.recv_fds(ours, 1, 1)
except OSError:
return
if fds:
# This is our new stderr. We have to be careful not to leak it.
try:
stderr_fd, = fds
try:
os.dup2(stderr_fd, 2)
finally:
os.close(stderr_fd)
# We're about to abruptly drop our end of the stderr socketpair that we
# share with the ferny agent. ferny would normally treat that as an
# unexpected error. Instruct it to do a clean exit, instead.
interaction_client.command(2, 'ferny.end')
os.dup2(stderr_fd, 2)
finally:
for fd in fds:
os.close(fd)
def setup_logging(*, debug: bool):

View File

@ -20,7 +20,8 @@ import contextlib
import getpass
import logging
import os
from typing import Dict, List, Optional, Sequence, Union
import socket
from typing import Dict, List, Optional, Sequence, Tuple, Union
from cockpit._vendor import ferny
from cockpit._vendor.systemd_ctypes import bus
@ -57,7 +58,15 @@ class SuperuserPeer(ConfiguredPeer):
return transport
class AuthorizeResponder(ferny.InteractionResponder):
class CockpitResponder(ferny.InteractionResponder):
async def do_custom_command(self, command: str, args: Tuple, fds: List[int], stderr: str) -> None:
if command == 'cockpit.send-stderr':
with socket.socket(fileno=fds[0]) as sock:
fds.pop(0)
socket.send_fds(sock, [b'\0'], [2])
class AuthorizeResponder(CockpitResponder):
def __init__(self, router: Router):
self.router = router
@ -66,7 +75,7 @@ class AuthorizeResponder(ferny.InteractionResponder):
return await self.router.request_authorization(f'plain1:{hexuser}')
class SuperuserRoutingRule(RoutingRule, ferny.InteractionResponder, bus.Object, interface='cockpit.Superuser'):
class SuperuserRoutingRule(RoutingRule, CockpitResponder, bus.Object, interface='cockpit.Superuser'):
superuser_configs: Dict[str, Dict[str, object]]
pending_prompt: Optional[asyncio.Future]
peer: Optional[Peer]

View File

@ -7,7 +7,7 @@ pw = os.environ.get('PSEUDO_PASSWORD')
if pw:
reader, writer = os.pipe()
# '-' is the (ignored) argv[0], and 'can haz pw' is the message in argv[1]
interaction_client.interact(2, writer, ['-', 'can haz pw?'], {})
interaction_client.askpass(2, writer, ['-', 'can haz pw?'], {})
os.close(writer)
response = os.read(reader, 1024).decode('utf-8').strip()