1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-10 01:48:00 +00:00
putty-source/utils/tempseat.c
Simon Tatham 6d272ee007 Allow new_connection to take an optional Seat. (NFC)
This is working towards allowing the subsidiary SSH connection in an
SshProxy to share the main user-facing Seat, so as to be able to pass
through interactive prompts.

This is more difficult than the similar change with LogPolicy, because
Seats are stateful. In particular, the trust-sigil status will need to
be controlled by the SshProxy until it's ready to pass over control to
the main SSH (or whatever) connection.

To make this work, I've introduced a thing called a TempSeat, which is
(yet) another Seat implementation. When a backend hands its Seat to
new_connection(), it does it in a way that allows new_connection() to
borrow it completely, and replace it in the main backend structure
with a TempSeat, which acts as a temporary placeholder. If the main
backend tries to do things like changing trust status or sending
output, the TempSeat will buffer them; later on, when the connection
is established, TempSeat will replay the changes into the real Seat.

So, in each backend, I've made the following changes:
 - pass &foo->seat to new_connection, which may overwrite it with a
   TempSeat.
 - if it has done so (which we can tell via the is_tempseat() query
   function), then we have to free the TempSeat and reinstate our main
   Seat. The signal that we can do so is the PLUGLOG_CONNECT_SUCCESS
   notification, which indicates that SshProxy has finished all its
   connection setup work.
 - we also have to remember to free the TempSeat if our backend is
   disposed of without that having happened (e.g. because the
   connection _doesn't_ succeed).
 - in backends which have no local auth phase to worry about, ensure
   we don't call seat_set_trust_status on the main Seat _before_ it
   gets potentially replaced with a TempSeat. Moved some calls of
   seat_set_trust_status to just after new_connection(), so that now
   the initial trust status setup will go into the TempSeat (if
   appropriate) and be buffered until that seat is relinquished.

In all other uses of new_connection, where we don't have a Seat
available at all, we just pass NULL.

This is NFC, because neither new_connection() nor any of its delegates
will _actually_ do this replacement yet. We're just setting up the
framework to enable it to do so in the next commit.
2021-09-13 17:24:47 +01:00

354 lines
12 KiB
C

/*
* Implementation of the Seat trait that buffers output and other
* events until it can give them back to a real Seat.
*
* This is used by the SSH proxying code, which temporarily takes over
* the real user-facing Seat so that it can issue host key warnings,
* password prompts etc for the proxy SSH connection. While it's got
* the real Seat, it gives the primary connection's backend one of
* these temporary Seats in the interim, so that if the backend wants
* to send some kind of initial output, or start by reconfiguring the
* trust status, or what have you, then it can do that without having
* to keep careful track of the fact that its Seat is out on loan.
*/
#include "putty.h"
typedef struct TempSeat TempSeat;
struct TempSeat {
Seat *realseat;
bufchain outputs[2]; /* stdout, stderr */
bool seen_session_started;
bool seen_remote_exit;
bool seen_remote_disconnect;
bool seen_update_specials_menu;
bool seen_echoedit_update, echoing, editing;
bool seen_trust_status, trusted;
Seat seat;
};
/* ----------------------------------------------------------------------
* Methods we can usefully buffer, and pass their results on to the
* real Seat in tempseat_flush().
*/
static size_t tempseat_output(Seat *seat, bool is_stderr, const void *data,
size_t len)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
bufchain_add(&ts->outputs[is_stderr], data, len);
return bufchain_size(&ts->outputs[0]) + bufchain_size(&ts->outputs[1]);
}
static void tempseat_notify_session_started(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
ts->seen_session_started = true;
}
static void tempseat_notify_remote_exit(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
ts->seen_remote_exit = true;
}
static void tempseat_notify_remote_disconnect(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
ts->seen_remote_disconnect = true;
}
static void tempseat_update_specials_menu(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
ts->seen_update_specials_menu = true;
}
static void tempseat_echoedit_update(Seat *seat, bool echoing, bool editing)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
ts->seen_echoedit_update = true;
ts->echoing = echoing;
ts->editing = editing;
}
static void tempseat_set_trust_status(Seat *seat, bool trusted)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
ts->seen_trust_status = true;
ts->trusted = trusted;
}
/* ----------------------------------------------------------------------
* Methods we can safely pass straight on to the real Seat, usually
* (but not in every case) because they're read-only queries.
*/
static char *tempseat_get_ttymode(Seat *seat, const char *mode)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_get_ttymode(ts->realseat, mode);
}
static void tempseat_set_busy_status(Seat *seat, BusyStatus status)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
/*
* set_busy_status is generally called when something is about to
* do some single-threaded, event-loop blocking computation. This
* _shouldn't_ happen in a backend while it's waiting for a
* network connection to be made, but if for some reason it were
* to, there's no reason we can't just pass this straight to the
* real seat, because we expect that it will mark itself busy,
* compute, and mark itself unbusy, all between yields to the
* event loop that might give whatever else is using the real Seat
* an opportunity to do anything.
*/
seat_set_busy_status(ts->realseat, status);
}
static bool tempseat_is_utf8(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_is_utf8(ts->realseat);
}
static const char *tempseat_get_x_display(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_get_x_display(ts->realseat);
}
static bool tempseat_get_windowid(Seat *seat, long *id_out)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_get_windowid(ts->realseat, id_out);
}
static bool tempseat_get_window_pixel_size(Seat *seat, int *width, int *height)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_get_window_pixel_size(ts->realseat, width, height);
}
static StripCtrlChars *tempseat_stripctrl_new(
Seat *seat, BinarySink *bs_out, SeatInteractionContext sic)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_stripctrl_new(ts->realseat, bs_out, sic);
}
static bool tempseat_verbose(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_verbose(ts->realseat);
}
static bool tempseat_interactive(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_interactive(ts->realseat);
}
static bool tempseat_get_cursor_position(Seat *seat, int *x, int *y)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_get_cursor_position(ts->realseat, x, y);
}
static bool tempseat_can_set_trust_status(Seat *seat)
{
TempSeat *ts = container_of(seat, TempSeat, seat);
return seat_can_set_trust_status(ts->realseat);
}
/* ----------------------------------------------------------------------
* Methods that should never be called on a TempSeat, so we can put an
* unreachable() in them.
*
* A backend in possession of a TempSeat ought to be sitting and
* patiently waiting for a network connection attempt to either
* succeed or fail. And it should be aware of the possibility that the
* proxy setup code to which it has lent the real Seat might need to
* present interactive prompts - that's the whole point of lending out
* the Seat in the first place - so it absolutely shouldn't get any
* ideas about issuing some kind of prompt of its own while it waits
* for the network connection.
*/
static int tempseat_get_userpass_input(Seat *seat, prompts_t *p,
bufchain *input)
{
/*
* Interactive prompts of this nature are a thing that a backend
* MUST NOT do while not in possession of the real Seat, because
* the whole point of temporarily lending the real Seat to
* something else is that so it can have a clear field to do
* interactive stuff of its own while making a network connection.
*/
unreachable("get_userpass_input should never be called on TempSeat");
}
static int tempseat_verify_ssh_host_key(
Seat *seat, const char *host, int port, const char *keytype,
char *keystr, const char *keydisp, char **key_fingerprints,
void (*callback)(void *ctx, int result), void *ctx)
{
unreachable("verify_ssh_host_key should never be called on TempSeat");
}
static int tempseat_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
void (*callback)(void *ctx, int result), void *ctx)
{
unreachable("confirm_weak_crypto_primitive "
"should never be called on TempSeat");
}
static int tempseat_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
void (*callback)(void *ctx, int result), void *ctx)
{
unreachable("confirm_weak_cached_hostkey "
"should never be called on TempSeat");
}
static void tempseat_connection_fatal(Seat *seat, const char *message)
{
/*
* Fatal errors are another thing a backend should not have any
* reason to encounter while waiting to hear back about its
* network connection setup.
*
* Also, if a backend _did_ call this, it would be hellish to
* unpick all the error handling. Just passing on the fatal error
* to the real Seat wouldn't be good enough: what about freeing
* all the various things that are confusingly holding pointers to
* each other? Better to leave this as an assertion-failure level
* issue, so that if it does ever happen by accident, we'll know
* it's a bug.
*/
unreachable("connection_fatal should never be called on TempSeat");
}
static bool tempseat_eof(Seat *seat)
{
/*
* EOF is _very nearly_ something that we could buffer, and pass
* on to the real Seat at flush time. The only difficulty is that
* sometimes the front end wants to respond to an incoming EOF by
* instructing the back end to send an outgoing one, which it does
* by returning a bool from its eof method.
*
* So we'd have to arrange that tempseat_flush caught that return
* value and passed it on to the calling backend. And then every
* backend would have to deal with tempseat_flush maybe returning
* it an 'actually, please start closing down now' indication,
* which could only happen _in theory_, if it had for some reason
* called seat_eof on the TempSeat.
*
* But in fact, we don't expect back ends to call seat_eof on the
* TempSeat in the first place, so all of that effort would be a
* total waste. Hence, we'll put EOF in the category of things we
* expect backends never to do while the real Seat is out on loan.
*/
unreachable("eof should never be called on TempSeat");
}
/* ----------------------------------------------------------------------
* Done with the TempSeat methods. Here's the vtable definition and
* the main setup/teardown code.
*/
static const struct SeatVtable tempseat_vt = {
.output = tempseat_output,
.eof = tempseat_eof,
.sent = nullseat_sent,
.get_userpass_input = tempseat_get_userpass_input,
.notify_session_started = tempseat_notify_session_started,
.notify_remote_exit = tempseat_notify_remote_exit,
.notify_remote_disconnect = tempseat_notify_remote_disconnect,
.connection_fatal = tempseat_connection_fatal,
.update_specials_menu = tempseat_update_specials_menu,
.get_ttymode = tempseat_get_ttymode,
.set_busy_status = tempseat_set_busy_status,
.verify_ssh_host_key = tempseat_verify_ssh_host_key,
.confirm_weak_crypto_primitive = tempseat_confirm_weak_crypto_primitive,
.confirm_weak_cached_hostkey = tempseat_confirm_weak_cached_hostkey,
.is_utf8 = tempseat_is_utf8,
.echoedit_update = tempseat_echoedit_update,
.get_x_display = tempseat_get_x_display,
.get_windowid = tempseat_get_windowid,
.get_window_pixel_size = tempseat_get_window_pixel_size,
.stripctrl_new = tempseat_stripctrl_new,
.set_trust_status = tempseat_set_trust_status,
.can_set_trust_status = tempseat_can_set_trust_status,
.verbose = tempseat_verbose,
.interactive = tempseat_interactive,
.get_cursor_position = tempseat_get_cursor_position,
};
Seat *tempseat_new(Seat *realseat)
{
TempSeat *ts = snew(TempSeat);
memset(ts, 0, sizeof(*ts));
ts->seat.vt = &tempseat_vt;
ts->realseat = realseat;
for (unsigned i = 0; i < 2; i++)
bufchain_init(&ts->outputs[i]);
return &ts->seat;
}
bool is_tempseat(Seat *seat)
{
return seat->vt == &tempseat_vt;
}
Seat *tempseat_get_real(Seat *seat)
{
assert(seat->vt == &tempseat_vt);
TempSeat *ts = container_of(seat, TempSeat, seat);
return ts->realseat;
}
void tempseat_free(Seat *seat)
{
assert(seat->vt == &tempseat_vt);
TempSeat *ts = container_of(seat, TempSeat, seat);
for (unsigned i = 0; i < 2; i++)
bufchain_clear(&ts->outputs[i]);
sfree(ts);
}
void tempseat_flush(Seat *seat)
{
assert(seat->vt == &tempseat_vt);
TempSeat *ts = container_of(seat, TempSeat, seat);
/* Empty the stdout/stderr bufchains into the real seat */
for (unsigned i = 0; i < 2; i++) {
while (bufchain_size(&ts->outputs[i])) {
ptrlen pl = bufchain_prefix(&ts->outputs[i]);
seat_output(ts->realseat, i, pl.ptr, pl.len);
bufchain_consume(&ts->outputs[i], pl.len);
}
}
/* Pass on any other kinds of event we've buffered */
if (ts->seen_session_started)
seat_notify_session_started(ts->realseat);
if (ts->seen_remote_exit)
seat_notify_remote_exit(ts->realseat);
if (ts->seen_remote_disconnect)
seat_notify_remote_disconnect(ts->realseat);
if (ts->seen_update_specials_menu)
seat_update_specials_menu(ts->realseat);
if (ts->seen_echoedit_update)
seat_echoedit_update(ts->realseat, ts->echoing, ts->editing);
if (ts->seen_trust_status)
seat_set_trust_status(ts->realseat, ts->trusted);
}