1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 09:27:59 +00:00

New system for reading prompts from the console.

Until now, the command-line PuTTY tools (PSCP, PSFTP and Plink) have
presented all the kinds of interactive prompt (password/passphrase,
host key, the assorted weak-crypto warnings, 'append to log file?') on
standard error, and read the responses from standard input.

This is unfortunate because if you're redirecting their standard
input (especially likely with Plink) then the prompt responses will
consume some of the intended session data. It would be better to
present the prompts _on the console_, even if that's not where stdin
or stderr point.

On Unix, we've been doing this for ages, by opening /dev/tty directly.
On Windows, we didn't, because I didn't know how. But I've recently
found out: you can open the magic file names CONIN$ and CONOUT$, which
will point at your actual console, if one is available.

So now, if it's possible, the command-line tools will do that. But if
the attempt to open CONIN$ and CONOUT$ fails, they'll fall back to the
old behaviour (in particular, if no console is available at all).

In order to make this happen consistently across all the prompt types,
I've introduced a new object called ConsoleIO, which holds whatever
file handles are necessary, knows whether to close them
afterwards (yes if they were obtained by opening CONFOO$, no if
they're the standard I/O handles), and presents a BinarySink API to
write to them and a custom API to read a line of text.

This seems likely to break _someone's_ workflow. So I've added an
option '-legacy-stdio-prompts' to restore the old behaviour.
This commit is contained in:
Simon Tatham 2022-11-24 12:46:25 +00:00
parent f91c3127ad
commit 80aed96286
5 changed files with 336 additions and 187 deletions

View File

@ -920,6 +920,16 @@ int cmdline_process_param(const char *p, char *value,
}
}
if (!strcmp(p, "-legacy-stdio-prompts") ||
!strcmp(p, "-legacy_stdio_prompts")) {
RETURN(1);
SAVEABLE(0);
if (!console_set_stdio_prompts(true)) {
cmdline_report_unavailable(p);
return ret;
}
}
#ifdef _WINDOWS
/*
* Cross-tool options only available on Windows.

View File

@ -2563,6 +2563,7 @@ bool have_ssh_host_key(const char *host, int port, const char *keytype);
*/
extern bool console_batch_mode, console_antispoof_prompt;
extern bool console_set_batch_mode(bool);
extern bool console_set_stdio_prompts(bool);
SeatPromptResult console_get_userpass_input(prompts_t *p);
bool is_interactive(void);
void console_print_error_msg(const char *prefix, const char *msg);

View File

@ -8,3 +8,8 @@ bool console_set_batch_mode(bool newvalue)
{
return false;
}
bool console_set_stdio_prompts(bool newvalue)
{
return false;
}

View File

@ -570,6 +570,14 @@ bool is_interactive(void)
return isatty(0);
}
bool console_set_stdio_prompts(bool newvalue)
{
/* Sending prompts to stdio in place of /dev/tty is not supported
* in the Unix tools. It's only supported on Windows because of
* years of history making it likely someone was depending on it. */
return false;
}
/*
* X11-forwarding-related things suitable for console.
*/

View File

@ -32,40 +32,244 @@ void console_print_error_msg(const char *prefix, const char *msg)
fflush(stderr);
}
/*
* System for getting I/O handles to talk to the console for
* interactive prompts.
*
* In PuTTY 0.78 and before, these prompts used the standard I/O
* handles. But this means you can't redirect Plink's actual stdin
* from a sensible data channel without the responses to login prompts
* unwantedly being read from it too.
*
* However, many versions of PuTTY have worked the old way, so we need
* a method of falling back to it for the sake of whoever's workflow
* it turns out to break. So this structure equivocates between the
* two systems.
*/
static bool conio_use_standard_handles = false;
bool console_set_stdio_prompts(bool newvalue)
{
conio_use_standard_handles = newvalue;
return true;
}
typedef struct ConsoleIO {
HANDLE hin, hout;
bool need_close_hin, need_close_hout;
bool hin_is_console, hout_is_console;
BinarySink_IMPLEMENTATION;
} ConsoleIO;
static void console_write(BinarySink *bs, const void *data, size_t len);
static ConsoleIO *conio_setup(void)
{
ConsoleIO *conio = snew(ConsoleIO);
conio->hin = conio->hout = INVALID_HANDLE_VALUE;
conio->need_close_hin = conio->need_close_hout = false;
/*
* First try opening the console itself, so that prompts will go
* there regardless of I/O redirection. We don't do this if the
* user has deliberately requested a fallback to the old
* behaviour. We also don't do it in batch mode, because in that
* situation, any need for an interactive prompt will instead
* noninteractively abort the connection, and in that situation,
* the 'prompt' becomes more in the nature of an error message, so
* it should go to standard error like everything else.
*/
if (!conio_use_standard_handles && !console_batch_mode) {
/*
* If we do open the console, it has to be done separately for
* input and output, with different magic file names.
*
* We need both read and write permission for both handles,
* because read permission is needed to read the console mode
* (in particular, to test if a file handle _is_ a console),
* and write permission to change it.
*/
conio->hin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (conio->hin != INVALID_HANDLE_VALUE)
conio->need_close_hin = true;
conio->hout = CreateFile("CONOUT$", GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (conio->hout != INVALID_HANDLE_VALUE)
conio->need_close_hout = true;
}
/*
* Fall back from that to using the standard handles. We use
* standard error rather than standard output for our prompts,
* because that has a better chance of separating them from
*/
if (conio->hin == INVALID_HANDLE_VALUE)
conio->hin = GetStdHandle(STD_INPUT_HANDLE);
if (conio->hout == INVALID_HANDLE_VALUE)
conio->hout = GetStdHandle(STD_INPUT_HANDLE);
DWORD dummy;
conio->hin_is_console = GetConsoleMode(conio->hin, &dummy);
conio->hout_is_console = GetConsoleMode(conio->hout, &dummy);
BinarySink_INIT(conio, console_write);
return conio;
}
static void conio_free(ConsoleIO *conio)
{
if (conio->need_close_hin)
CloseHandle(conio->hin);
if (conio->need_close_hout)
CloseHandle(conio->hout);
sfree(conio);
}
static void console_write(BinarySink *bs, const void *data, size_t len)
{
ConsoleIO *conio = BinarySink_DOWNCAST(bs, ConsoleIO);
const char *cdata = (const char *)data;
size_t pos = 0;
DWORD nwritten;
while (pos < len && WriteFile(conio->hout, cdata+pos, len-pos,
&nwritten, NULL))
pos += nwritten;
}
static bool console_read_line_to_strbuf(ConsoleIO *conio, bool echo,
strbuf *sb)
{
DWORD savemode;
if (conio->hin_is_console) {
GetConsoleMode(conio->hin, &savemode);
DWORD newmode = savemode | ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT;
if (!echo)
newmode &= ~ENABLE_ECHO_INPUT;
else
newmode |= ENABLE_ECHO_INPUT;
SetConsoleMode(conio->hin, newmode);
}
bool toret = false;
while (true) {
if (ptrlen_endswith(ptrlen_from_strbuf(sb),
PTRLEN_LITERAL("\n"), NULL)) {
toret = true;
goto out;
}
char buf[4096];
DWORD nread;
if (!ReadFile(conio->hin, buf, lenof(buf), &nread, NULL))
goto out;
put_data(sb, buf, nread);
smemclr(buf, sizeof(buf));
}
out:
if (!echo)
put_datalit(conio, "\r\n");
if (conio->hin_is_console)
SetConsoleMode(conio->hin, savemode);
return toret;
}
static char *console_read_line(ConsoleIO *conio, bool echo)
{
strbuf *sb = strbuf_new_nm();
if (!console_read_line_to_strbuf(conio, echo, sb)) {
strbuf_free(sb);
return NULL;
} else {
return strbuf_to_str(sb);
}
}
typedef enum {
RESPONSE_ABANDON,
RESPONSE_YES,
RESPONSE_NO,
RESPONSE_INFO,
RESPONSE_UNRECOGNISED
} ResponseType;
static ResponseType parse_and_free_response(char *line)
{
if (!line)
return RESPONSE_ABANDON;
ResponseType toret;
switch (line[0]) {
/* In case of misplaced reflexes from another program,
* recognise 'q' as 'abandon connection' as well as the
* advertised 'just press Return' */
case 'q':
case 'Q':
case '\n':
case '\r':
case '\0':
toret = RESPONSE_ABANDON;
break;
case 'y':
case 'Y':
toret = RESPONSE_YES;
break;
case 'n':
case 'N':
toret = RESPONSE_NO;
break;
case 'i':
case 'I':
toret = RESPONSE_INFO;
break;
default:
toret = RESPONSE_UNRECOGNISED;
break;
}
burnstr(line);
return toret;
}
SeatPromptResult console_confirm_ssh_host_key(
Seat *seat, const char *host, int port, const char *keytype,
char *keystr, SeatDialogText *text, HelpCtx helpctx,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
HANDLE hin;
DWORD savemode, i;
ConsoleIO *conio = conio_setup();
const char *prompt = NULL;
stdio_sink errsink[1];
stdio_sink_init(errsink, stderr);
char line[32];
SeatPromptResult result;
for (SeatDialogTextItem *item = text->items,
*end = item+text->nitems; item < end; item++) {
switch (item->type) {
case SDT_PARA:
wordwrap(BinarySink_UPCAST(errsink),
wordwrap(BinarySink_UPCAST(conio),
ptrlen_from_asciz(item->text), 60);
fputc('\n', stderr);
put_byte(conio, '\n');
break;
case SDT_DISPLAY:
fprintf(stderr, " %s\n", item->text);
put_fmt(conio, " %s\n", item->text);
break;
case SDT_SCARY_HEADING:
/* Can't change font size or weight in this context */
fprintf(stderr, "%s\n", item->text);
put_fmt(conio, "%s\n", item->text);
break;
case SDT_BATCH_ABORT:
if (console_batch_mode) {
fprintf(stderr, "%s\n", item->text);
fflush(stderr);
return SPR_SW_ABORT("Cannot confirm a host key in batch mode");
put_fmt(conio, "%s\n", item->text);
result = SPR_SW_ABORT(
"Cannot confirm a host key in batch mode");
goto out;
}
break;
case SDT_PROMPT:
@ -77,33 +281,26 @@ SeatPromptResult console_confirm_ssh_host_key(
}
assert(prompt); /* something in the SeatDialogText should have set this */
ResponseType response;
while (true) {
fprintf(stderr,
"%s (y/n, Return cancels connection, i for more info) ",
put_fmt(conio, "%s (y/n, Return cancels connection, i for more info) ",
prompt);
fflush(stderr);
line[0] = '\0'; /* fail safe if ReadFile returns no data */
response = parse_and_free_response(console_read_line(conio, true));
hin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hin, &savemode);
SetConsoleMode(hin, (savemode | ENABLE_ECHO_INPUT |
ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT));
ReadFile(hin, line, sizeof(line) - 1, &i, NULL);
SetConsoleMode(hin, savemode);
if (line[0] == 'i' || line[0] == 'I') {
if (response == RESPONSE_INFO) {
for (SeatDialogTextItem *item = text->items,
*end = item+text->nitems; item < end; item++) {
switch (item->type) {
case SDT_MORE_INFO_KEY:
fprintf(stderr, "%s", item->text);
put_dataz(conio, item->text);
break;
case SDT_MORE_INFO_VALUE_SHORT:
fprintf(stderr, ": %s\n", item->text);
put_fmt(conio, ": %s\n", item->text);
break;
case SDT_MORE_INFO_VALUE_BLOB:
fprintf(stderr, ":\n%s\n", item->text);
put_fmt(conio, ":\n%s\n", item->text);
break;
default:
break;
@ -114,92 +311,89 @@ SeatPromptResult console_confirm_ssh_host_key(
}
}
/* In case of misplaced reflexes from another program, also recognise 'q'
* as 'abandon connection rather than trust this key' */
if (line[0] != '\0' && line[0] != '\r' && line[0] != '\n' &&
line[0] != 'q' && line[0] != 'Q') {
if (line[0] == 'y' || line[0] == 'Y')
if (response == RESPONSE_YES || response == RESPONSE_NO) {
if (response == RESPONSE_YES)
store_host_key(seat, host, port, keytype, keystr);
return SPR_OK;
result = SPR_OK;
} else {
fputs(console_abandoned_msg, stderr);
return SPR_USER_ABORT;
put_dataz(conio, console_abandoned_msg);
result = SPR_USER_ABORT;
}
out:
conio_free(conio);
return result;
}
SeatPromptResult console_confirm_weak_crypto_primitive(
Seat *seat, const char *algtype, const char *algname,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
HANDLE hin;
DWORD savemode, i;
ConsoleIO *conio = conio_setup();
SeatPromptResult result;
char line[32];
fprintf(stderr, weakcrypto_msg_common_fmt, algtype, algname);
put_fmt(conio, weakcrypto_msg_common_fmt, algtype, algname);
if (console_batch_mode) {
fputs(console_abandoned_msg, stderr);
return SPR_SW_ABORT("Cannot confirm a weak crypto primitive "
"in batch mode");
put_dataz(conio, console_abandoned_msg);
result = SPR_SW_ABORT("Cannot confirm a weak crypto primitive "
"in batch mode");
goto out;
}
fputs(console_continue_prompt, stderr);
fflush(stderr);
put_dataz(conio, console_continue_prompt);
hin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hin, &savemode);
SetConsoleMode(hin, (savemode | ENABLE_ECHO_INPUT |
ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT));
ReadFile(hin, line, sizeof(line) - 1, &i, NULL);
SetConsoleMode(hin, savemode);
ResponseType response = parse_and_free_response(
console_read_line(conio, true));
if (line[0] == 'y' || line[0] == 'Y') {
return SPR_OK;
if (response == RESPONSE_YES) {
result = SPR_OK;
} else {
fputs(console_abandoned_msg, stderr);
return SPR_USER_ABORT;
put_dataz(conio, console_abandoned_msg);
result = SPR_USER_ABORT;
}
out:
conio_free(conio);
return result;
}
SeatPromptResult console_confirm_weak_cached_hostkey(
Seat *seat, const char *algname, const char *betteralgs,
void (*callback)(void *ctx, SeatPromptResult result), void *ctx)
{
HANDLE hin;
DWORD savemode, i;
ConsoleIO *conio = conio_setup();
SeatPromptResult result;
char line[32];
fprintf(stderr, weakhk_msg_common_fmt, algname, betteralgs);
put_fmt(conio, weakhk_msg_common_fmt, algname, betteralgs);
if (console_batch_mode) {
fputs(console_abandoned_msg, stderr);
return SPR_SW_ABORT("Cannot confirm a weak cached host key "
"in batch mode");
put_dataz(conio, console_abandoned_msg);
result = SPR_SW_ABORT("Cannot confirm a weak cached host key "
"in batch mode");
goto out;
}
fputs(console_continue_prompt, stderr);
fflush(stderr);
put_dataz(conio, console_continue_prompt);
hin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hin, &savemode);
SetConsoleMode(hin, (savemode | ENABLE_ECHO_INPUT |
ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT));
ReadFile(hin, line, sizeof(line) - 1, &i, NULL);
SetConsoleMode(hin, savemode);
ResponseType response = parse_and_free_response(
console_read_line(conio, true));
if (line[0] == 'y' || line[0] == 'Y') {
return SPR_OK;
if (response == RESPONSE_YES) {
result = SPR_OK;
} else {
fputs(console_abandoned_msg, stderr);
return SPR_USER_ABORT;
put_dataz(conio, console_abandoned_msg);
result = SPR_USER_ABORT;
}
out:
conio_free(conio);
return result;
}
bool is_interactive(void)
{
return is_console_handle(GetStdHandle(STD_INPUT_HANDLE));
ConsoleIO *conio = conio_setup();
bool toret = conio->hin_is_console;
conio_free(conio);
return toret;
}
bool console_antispoof_prompt = true;
@ -250,9 +444,6 @@ bool console_has_mixed_input_stream(Seat *seat)
int console_askappend(LogPolicy *lp, Filename *filename,
void (*callback)(void *ctx, int result), void *ctx)
{
HANDLE hin;
DWORD savemode, i;
static const char msgtemplate[] =
"The session log file \"%.*s\" already exists.\n"
"You can overwrite it with a new session log,\n"
@ -266,29 +457,28 @@ int console_askappend(LogPolicy *lp, Filename *filename,
"The session log file \"%.*s\" already exists.\n"
"Logging will not be enabled.\n";
char line[32];
ConsoleIO *conio = conio_setup();
int result;
if (console_batch_mode) {
fprintf(stderr, msgtemplate_batch, FILENAME_MAX, filename->path);
fflush(stderr);
return 0;
put_fmt(conio, msgtemplate_batch, FILENAME_MAX, filename->path);
result = 0;
goto out;
}
fprintf(stderr, msgtemplate, FILENAME_MAX, filename->path);
fflush(stderr);
put_fmt(conio, msgtemplate, FILENAME_MAX, filename->path);
hin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hin, &savemode);
SetConsoleMode(hin, (savemode | ENABLE_ECHO_INPUT |
ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT));
ReadFile(hin, line, sizeof(line) - 1, &i, NULL);
SetConsoleMode(hin, savemode);
ResponseType response = parse_and_free_response(
console_read_line(conio, true));
if (line[0] == 'y' || line[0] == 'Y')
return 2;
else if (line[0] == 'n' || line[0] == 'N')
return 1;
if (response == RESPONSE_YES)
result = 2;
else if (response == RESPONSE_NO)
result = 1;
else
return 0;
result = 0;
out:
conio_free(conio);
return result;
}
/*
@ -357,15 +547,10 @@ StripCtrlChars *console_stripctrl_new(
return stripctrl_new(bs_out, false, 0);
}
static void console_write(HANDLE hout, ptrlen data)
{
DWORD dummy;
WriteFile(hout, data.ptr, data.len, &dummy, NULL);
}
SeatPromptResult console_get_userpass_input(prompts_t *p)
{
HANDLE hin = INVALID_HANDLE_VALUE, hout = INVALID_HANDLE_VALUE;
ConsoleIO *conio = conio_setup();
SeatPromptResult result;
size_t curr_prompt;
/*
@ -384,24 +569,10 @@ SeatPromptResult console_get_userpass_input(prompts_t *p)
* need to ensure that we're able to get the answers.
*/
if (p->n_prompts) {
if (console_batch_mode)
return SPR_SW_ABORT("Cannot answer interactive prompts "
"in batch mode");
hin = GetStdHandle(STD_INPUT_HANDLE);
if (hin == INVALID_HANDLE_VALUE) {
fprintf(stderr, "Cannot get standard input handle\n");
cleanup_exit(1);
}
}
/*
* And if we have anything to print, we need standard output.
*/
if ((p->name_reqd && p->name) || p->instruction || p->n_prompts) {
hout = GetStdHandle(STD_OUTPUT_HANDLE);
if (hout == INVALID_HANDLE_VALUE) {
fprintf(stderr, "Cannot get standard output handle\n");
cleanup_exit(1);
if (console_batch_mode) {
result = SPR_SW_ABORT("Cannot answer interactive prompts "
"in batch mode");
goto out;
}
}
@ -411,86 +582,40 @@ SeatPromptResult console_get_userpass_input(prompts_t *p)
/* We only print the `name' caption if we have to... */
if (p->name_reqd && p->name) {
ptrlen plname = ptrlen_from_asciz(p->name);
console_write(hout, plname);
put_datapl(conio, plname);
if (!ptrlen_endswith(plname, PTRLEN_LITERAL("\n"), NULL))
console_write(hout, PTRLEN_LITERAL("\n"));
put_datalit(conio, "\n");
}
/* ...but we always print any `instruction'. */
if (p->instruction) {
ptrlen plinst = ptrlen_from_asciz(p->instruction);
console_write(hout, plinst);
put_datapl(conio, plinst);
if (!ptrlen_endswith(plinst, PTRLEN_LITERAL("\n"), NULL))
console_write(hout, PTRLEN_LITERAL("\n"));
put_datalit(conio, "\n");
}
for (curr_prompt = 0; curr_prompt < p->n_prompts; curr_prompt++) {
DWORD savemode, newmode;
prompt_t *pr = p->prompts[curr_prompt];
GetConsoleMode(hin, &savemode);
newmode = savemode | ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT;
if (!pr->echo)
newmode &= ~ENABLE_ECHO_INPUT;
else
newmode |= ENABLE_ECHO_INPUT;
SetConsoleMode(hin, newmode);
put_dataz(conio, pr->prompt);
console_write(hout, ptrlen_from_asciz(pr->prompt));
bool failed = false;
SeatPromptResult spr;
while (1) {
/*
* Amount of data to try to read from the console in one
* go. This isn't completely arbitrary: a user reported
* that trying to read more than 31366 bytes at a time
* would fail with ERROR_NOT_ENOUGH_MEMORY on Windows 7,
* and Ruby's Win32 support module has evidence of a
* similar workaround:
*
* https://github.com/ruby/ruby/blob/0aa5195262d4193d3accf3e6b9bad236238b816b/win32/win32.c#L6842
*
* To keep things simple, I stick with a nice round power
* of 2 rather than trying to go to the very limit of that
* bug. (We're typically reading user passphrases and the
* like here, so even this much is overkill really.)
*/
DWORD toread = 16384;
size_t prev_result_len = pr->result->len;
void *ptr = strbuf_append(pr->result, toread);
DWORD ret = 0;
if (!ReadFile(hin, ptr, toread, &ret, NULL)) {
/* An OS error when reading from the console is treated as an
* unexpected error and reported to the user. */
failed = true;
spr = make_spr_sw_abort_winerror(
"Error reading from console", GetLastError());
break;
} else if (ret == 0) {
/* Regard EOF on the terminal as a deliberate user-abort */
failed = true;
spr = SPR_USER_ABORT;
break;
}
strbuf_shrink_to(pr->result, prev_result_len + ret);
if (!console_read_line_to_strbuf(conio, pr->echo, pr->result)) {
result = make_spr_sw_abort_winerror(
"Error reading from console", GetLastError());
goto out;
} else if (!pr->result->len) {
/* Regard EOF on the terminal as a deliberate user-abort */
result = SPR_USER_ABORT;
goto out;
} else {
if (strbuf_chomp(pr->result, '\n')) {
strbuf_chomp(pr->result, '\r');
break;
}
}
SetConsoleMode(hin, savemode);
if (!pr->echo)
console_write(hout, PTRLEN_LITERAL("\r\n"));
if (failed)
return spr;
}
return SPR_OK;
result = SPR_OK;
out:
conio_free(conio);
return result;
}