1
0
mirror of git://git.gnupg.org/gnupg.git synced 2024-10-31 20:08:43 +01:00
gnupg/common/ttyio.c
Werner Koch edfe9453be
w32: Add fallback in case the Windows console can't cope with Unicode.
* common/ttyio.c (w32_write_console): Fallback to WriteConsoleA on
error.
--

To test this switch the Windows Console to "legacy mode"

  set LANG=de
  gpg --card-edit

and enter an invalid command.  The response contains an Umlaut and old
Windows versions (and the legacy console) don't have a proper font
installed for this.  Without this patch this runs into a log_fatal
error.

The mitigation we implement is to fallback to WriteConsoleA, that is
accepting wrong encoding and to print a note about the problem.

GnuPG-bug-id: 5491
2021-06-22 11:12:28 +02:00

788 lines
18 KiB
C

/* ttyio.c - tty i/O functions
* Copyright (C) 1997-2019 Werner Koch
* Copyright (C) 1998-2020 Free Software Foundation, Inc.
* Copyright (C) 2015-2020 g10 Code GmbH
*
* This file is part of GnuPG.
*
* This file is free software; you can redistribute it and/or modify
* it under the terms of either
*
* - the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 3 of the License, or (at
* your option) any later version.
*
* or
*
* - the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* or both in parallel, as here.
*
* This file is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <https://www.gnu.org/licenses/>.
* SPDX-License-Identifier: (LGPL-3.0-or-later OR GPL-2.0-or-later)
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#ifdef HAVE_TCGETATTR
# include <termios.h>
#else
# ifdef HAVE_TERMIO_H
/* simulate termios with termio */
# include <termio.h>
# define termios termio
# define tcsetattr ioctl
# define TCSAFLUSH TCSETAF
# define tcgetattr(A,B) ioctl(A,TCGETA,B)
# define HAVE_TCGETATTR
# endif
#endif
#ifdef HAVE_W32_SYSTEM
# ifdef HAVE_WINSOCK2_H
# include <winsock2.h>
# endif
# include <windows.h>
# ifdef HAVE_TCGETATTR
# error mingw32 and termios
# endif
#endif
#include <errno.h>
#include <ctype.h>
#include "util.h"
#include "ttyio.h"
#include "i18n.h"
#include "common-defs.h"
#define CONTROL_D ('D' - 'A' + 1)
#ifdef HAVE_W32_SYSTEM
static struct {
HANDLE in, out;
} con;
#define DEF_INPMODE (ENABLE_LINE_INPUT|ENABLE_ECHO_INPUT \
|ENABLE_PROCESSED_INPUT )
#define HID_INPMODE (ENABLE_LINE_INPUT|ENABLE_PROCESSED_INPUT )
#define DEF_OUTMODE (ENABLE_WRAP_AT_EOL_OUTPUT|ENABLE_PROCESSED_OUTPUT)
#else /* Unix */
static FILE *ttyfp = NULL;
#endif /* Unix */
static int initialized;
static int last_prompt_len;
static int batchmode;
static int no_terminal;
#ifdef HAVE_TCGETATTR
static struct termios termsave;
static int restore_termios;
#endif
/* Hooks set by gpgrlhelp.c if required. */
static void (*my_rl_set_completer) (rl_completion_func_t *);
static void (*my_rl_inhibit_completion) (int);
static void (*my_rl_cleanup_after_signal) (void);
static void (*my_rl_init_stream) (FILE *);
static char *(*my_rl_readline) (const char*);
static void (*my_rl_add_history) (const char*);
static int (*my_rl_rw_history)(const char *, int, int);
/* This is a wrapper around ttyname so that we can use it even when
the standard streams are redirected. It figures the name out the
first time and returns it in a statically allocated buffer. */
const char *
tty_get_ttyname (void)
{
static char *name;
/* On a GNU system ctermid() always return /dev/tty, so this does
not make much sense - however if it is ever changed we do the
Right Thing now. */
#ifdef HAVE_CTERMID
static int got_name;
if (!got_name)
{
const char *s;
/* Note that despite our checks for these macros the function is
not necessarily thread save. We mainly do this for
portability reasons, in case L_ctermid is not defined. */
# if defined(_POSIX_THREAD_SAFE_FUNCTIONS) || defined(_POSIX_TRHEADS)
char buffer[L_ctermid];
s = ctermid (buffer);
# else
s = ctermid (NULL);
# endif
if (s)
name = strdup (s);
got_name = 1;
}
#endif /*HAVE_CTERMID*/
/* Assume the standard tty on memory error or when there is no
ctermid. */
return name? name : "/dev/tty";
}
#ifdef HAVE_TCGETATTR
static void
cleanup(void)
{
if (restore_termios)
{
restore_termios = 0; /* do it prior in case it is interrupted again */
if (tcsetattr(fileno(ttyfp), TCSAFLUSH, &termsave))
log_error ("tcsetattr() failed: %s\n", strerror (errno));
}
}
#endif /*HAVE_TCGETATTR*/
static void
init_ttyfp(void)
{
if (initialized)
return;
#ifdef HAVE_W32_SYSTEM
{
SECURITY_ATTRIBUTES sa;
memset (&sa, 0, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
con.out = CreateFileA ("CONOUT$", GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
&sa, OPEN_EXISTING, 0, 0 );
if (con.out == INVALID_HANDLE_VALUE)
log_fatal ("open(CONOUT$) failed: %s\n", w32_strerror (-1));
memset (&sa, 0, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
con.in = CreateFileA ("CONIN$", GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
&sa, OPEN_EXISTING, 0, 0 );
if (con.in == INVALID_HANDLE_VALUE)
log_fatal ("open(CONIN$) failed: %s\n", w32_strerror (-1));
}
SetConsoleMode (con.in, DEF_INPMODE);
SetConsoleMode (con.out, DEF_OUTMODE);
#else /* Unix */
ttyfp = batchmode? stderr : fopen (tty_get_ttyname (), "r+");
if (!ttyfp)
{
log_error ("cannot open '%s': %s\n", tty_get_ttyname (), strerror(errno));
exit (2);
}
if (my_rl_init_stream)
my_rl_init_stream (ttyfp);
#endif /* Unix */
#ifdef HAVE_TCGETATTR
atexit (cleanup);
#endif
initialized = 1;
}
int
tty_batchmode( int onoff )
{
int old = batchmode;
if (onoff != -1)
batchmode = onoff;
return old;
}
int
tty_no_terminal(int onoff)
{
int old = no_terminal;
no_terminal = onoff ? 1 : 0;
return old;
}
#ifdef HAVE_W32_SYSTEM
/* Write the UTF-8 encoded STRING to the console. */
static void
w32_write_console (const char *string)
{
wchar_t *wstring;
DWORD n, nwritten;
wstring = utf8_to_wchar (string);
if (!wstring)
log_fatal ("w32_write_console failed: %s", strerror (errno));
n = wcslen (wstring);
if (!WriteConsoleW (con.out, wstring, n, &nwritten, NULL))
{
static int shown;
if (!shown)
{
shown = 1;
log_info ("WriteConsole failed: %s", w32_strerror (-1));
log_info ("Please configure a suitable font for the console\n");
}
n = strlen (string);
if (!WriteConsoleA (con.out, string, n , &nwritten, NULL))
log_fatal ("WriteConsole fallback failed: %s", w32_strerror (-1));
}
else
{
if (n != nwritten)
log_fatal ("WriteConsole failed: %lu != %lu\n",
(unsigned long)n, (unsigned long)nwritten);
}
last_prompt_len += n;
xfree (wstring);
}
#endif /*HAVE_W32_SYSTEM*/
void
tty_printf (const char *fmt, ... )
{
va_list arg_ptr;
if (no_terminal)
return;
if (!initialized)
init_ttyfp ();
va_start (arg_ptr, fmt);
#ifdef HAVE_W32_SYSTEM
{
char *buf = NULL;
vasprintf(&buf, fmt, arg_ptr);
if (!buf)
log_bug ("vasprintf() failed\n");
w32_write_console (buf);
xfree (buf);
}
#else /* Unix */
last_prompt_len += vfprintf (ttyfp, fmt, arg_ptr) ;
fflush (ttyfp);
#endif /* Unix */
va_end(arg_ptr);
}
/* Same as tty_printf but if FP is not NULL, behave like a regular
fprintf. */
void
tty_fprintf (estream_t fp, const char *fmt, ... )
{
va_list arg_ptr;
if (fp)
{
va_start (arg_ptr, fmt) ;
es_vfprintf (fp, fmt, arg_ptr );
va_end (arg_ptr);
return;
}
if (no_terminal)
return;
if (!initialized)
init_ttyfp ();
va_start (arg_ptr, fmt);
#ifdef HAVE_W32_SYSTEM
{
char *buf = NULL;
vasprintf (&buf, fmt, arg_ptr);
if (!buf)
log_bug ("vasprintf() failed\n");
w32_write_console (buf);
xfree (buf);
}
#else /* Unix */
last_prompt_len += vfprintf(ttyfp,fmt,arg_ptr) ;
fflush(ttyfp);
#endif /* Unix */
va_end(arg_ptr);
}
/* Print a string, but filter all control characters out. If FP is
* not NULL print to that stream instead to the tty. */
static void
do_print_string (estream_t fp, const byte *p, size_t n )
{
if (no_terminal && !fp)
return;
if (!initialized && !fp)
init_ttyfp();
if (fp)
{
print_utf8_buffer (fp, p, n);
return;
}
#ifdef HAVE_W32_SYSTEM
/* Not so effective, change it if you want */
for (; n; n--, p++)
{
if (iscntrl (*p))
{
if( *p == '\n' )
tty_printf ("\\n");
else if( !*p )
tty_printf ("\\0");
else
tty_printf ("\\x%02x", *p);
}
else
tty_printf ("%c", *p);
}
#else /* Unix */
for (; n; n--, p++)
{
if (iscntrl (*p))
{
putc ('\\', ttyfp);
if ( *p == '\n' )
putc ('n', ttyfp);
else if ( !*p )
putc ('0', ttyfp);
else
fprintf (ttyfp, "x%02x", *p );
}
else
putc (*p, ttyfp);
}
#endif /* Unix */
}
void
tty_print_utf8_string2 (estream_t fp, const byte *p, size_t n, size_t max_n)
{
size_t i;
char *buf;
if (no_terminal && !fp)
return;
/* We can handle plain ascii simpler, so check for it first. */
for(i=0; i < n; i++ )
{
if (p[i] & 0x80)
break;
}
if (i < n)
{
buf = utf8_to_native ((const char *)p, n, 0);
if (max_n && (strlen (buf) > max_n))
buf[max_n] = 0;
/* (utf8_to_native already did the control character quoting) */
tty_fprintf (fp, "%s", buf);
xfree (buf);
}
else
{
if (max_n && (n > max_n))
n = max_n;
do_print_string (fp, p, n );
}
}
void
tty_print_utf8_string (const byte *p, size_t n)
{
tty_print_utf8_string2 (NULL, p, n, 0);
}
/* Read a string from the tty using PROMPT. If HIDDEN is set the
* input is not echoed. */
static char *
do_get (const char *prompt, int hidden)
{
char *buf;
int n; /* Allocated size of BUF. */
int i; /* Number of bytes in BUF. */
int c;
#ifdef HAVE_W32_SYSTEM
char *utf8buf;
int errcount = 0;
#else
byte cbuf[1];
#endif
if (batchmode)
{
log_error (_("Sorry, we are in batchmode - can't get input\n"));
exit (2);
}
if (no_terminal)
{
log_error (_("Sorry, no terminal at all requested - can't get input\n"));
exit (2);
}
if (!initialized)
init_ttyfp ();
last_prompt_len = 0;
tty_printf ("%s", prompt);
buf = xmalloc ((n=50));
i = 0;
#ifdef HAVE_W32_SYSTEM
if (hidden)
SetConsoleMode(con.in, HID_INPMODE );
utf8buf = NULL;
for (;;)
{
DWORD nread;
wchar_t wbuf[2];
const unsigned char *s;
if (!ReadConsoleW (con.in, wbuf, 1, &nread, NULL))
log_fatal ("ReadConsole failed: %s", w32_strerror (-1));
if (!nread)
continue;
wbuf[1] = 0;
xfree (utf8buf);
utf8buf = wchar_to_utf8 (wbuf);
if (!utf8buf)
{
log_info ("wchar_to_utf8 failed: %s\n", strerror (errno));
if (++errcount > 10)
log_fatal (_("too many errors; giving up\n"));
continue;
}
if (*utf8buf == '\n')
{
if (utf8buf[1])
{
log_info ("ReadConsole returned more than requested"
" (0x0a,0x%02x)\n", utf8buf[1]);
if (++errcount > 10)
log_fatal (_("too many errors; giving up\n"));
}
break;
}
if (!hidden)
last_prompt_len++;
for (s=utf8buf; *s; s++)
{
c = *s;
if (c == '\t')
c = ' '; /* Map tab to a space. */
else if ((c >= 0 && c <= 0x1f) || c == 0x7f)
continue; /* Remove control characters. */
if (!(i < n-1))
{
n += 50;
buf = xrealloc (buf, n);
}
buf[i++] = c;
}
}
xfree (utf8buf);
if (hidden)
SetConsoleMode(con.in, DEF_INPMODE );
#else /* Unix */
if (hidden)
{
#ifdef HAVE_TCGETATTR
struct termios term;
if (tcgetattr(fileno(ttyfp), &termsave))
log_fatal ("tcgetattr() failed: %s\n", strerror(errno));
restore_termios = 1;
term = termsave;
term.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
if (tcsetattr( fileno(ttyfp), TCSAFLUSH, &term ) )
log_fatal("tcsetattr() failed: %s\n", strerror(errno));
#endif /*HAVE_TCGETATTR*/
}
/* fixme: How can we avoid that the \n is echoed w/o disabling
* canonical mode - w/o this kill_prompt can't work */
while (read(fileno(ttyfp), cbuf, 1) == 1 && *cbuf != '\n')
{
if (!hidden)
last_prompt_len++;
c = *cbuf;
if (c == CONTROL_D)
log_info (_("Control-D detected\n"));
if (c == '\t') /* Map tab to a space. */
c = ' ';
else if ( (c >= 0 && c <= 0x1f) || c == 0x7f)
continue; /* Skip all other ASCII control characters. */
if (!(i < n-1))
{
n += 50;
buf = xrealloc (buf, n);
}
buf[i++] = c;
}
if (*cbuf != '\n')
{
buf[0] = CONTROL_D;
i = 1;
}
if (hidden)
{
#ifdef HAVE_TCGETATTR
if (tcsetattr (fileno(ttyfp), TCSAFLUSH, &termsave))
log_error ("tcsetattr() failed: %s\n", strerror(errno));
restore_termios = 0;
#endif /*HAVE_TCGETATTR*/
}
#endif /* Unix */
buf[i] = 0;
return buf;
}
/* Note: This function never returns NULL. */
char *
tty_get( const char *prompt )
{
if (!batchmode && !no_terminal && my_rl_readline && my_rl_add_history)
{
char *line;
char *buf;
if (!initialized)
init_ttyfp();
last_prompt_len = 0;
line = my_rl_readline (prompt?prompt:"");
/* We need to copy it to memory controlled by our malloc
implementations; further we need to convert an EOF to our
convention. */
buf = xmalloc(line? strlen(line)+1:2);
if (line)
{
strcpy (buf, line);
trim_spaces (buf);
if (strlen (buf) > 2 )
my_rl_add_history (line); /* Note that we test BUF but add LINE. */
free (line);
}
else
{
buf[0] = CONTROL_D;
buf[1] = 0;
}
return buf;
}
else
return do_get ( prompt, 0 );
}
/* Variable argument version of tty_get. The prompt is actually a
* format string with arguments. */
char *
tty_getf (const char *promptfmt, ... )
{
va_list arg_ptr;
char *prompt;
char *answer;
va_start (arg_ptr, promptfmt);
if (gpgrt_vasprintf (&prompt, promptfmt, arg_ptr) < 0)
log_fatal ("estream_vasprintf failed: %s\n", strerror (errno));
va_end (arg_ptr);
answer = tty_get (prompt);
xfree (prompt);
return answer;
}
char *
tty_get_hidden( const char *prompt )
{
return do_get (prompt, 1);
}
void
tty_kill_prompt (void)
{
if (no_terminal)
return;
if (!initialized)
init_ttyfp ();
if (batchmode)
last_prompt_len = 0;
if (!last_prompt_len)
return;
#ifdef HAVE_W32_SYSTEM
tty_printf ("\r%*s\r", last_prompt_len, "");
#else /* Unix */
{
int i;
putc ('\r', ttyfp);
for (i=0; i < last_prompt_len; i ++ )
putc (' ', ttyfp);
putc ('\r', ttyfp);
fflush (ttyfp);
}
#endif /* Unix */
last_prompt_len = 0;
}
int
tty_get_answer_is_yes( const char *prompt )
{
int yes;
char *p;
p = tty_get (prompt);
tty_kill_prompt ();
yes = answer_is_yes (p);
xfree (p);
return yes;
}
/* Called by gnupg_rl_initialize to setup the readline support. */
void
tty_private_set_rl_hooks (void (*init_stream) (FILE *),
void (*set_completer) (rl_completion_func_t*),
void (*inhibit_completion) (int),
void (*cleanup_after_signal) (void),
char *(*readline_fun) (const char*),
void (*add_history_fun) (const char*),
int (*rw_history_fun)(const char *, int, int))
{
my_rl_init_stream = init_stream;
my_rl_set_completer = set_completer;
my_rl_inhibit_completion = inhibit_completion;
my_rl_cleanup_after_signal = cleanup_after_signal;
my_rl_readline = readline_fun;
my_rl_add_history = add_history_fun;
my_rl_rw_history = rw_history_fun;
}
/* Read the history from FILENAME or limit the size of the history.
* If FILENAME is NULL and NLINES is zero the current history is
* cleared. Returns 0 on success or -1 on error and sets ERRNO. No
* error is return if readline support is not available. */
int
tty_read_history (const char *filename, int nlines)
{
int rc;
if (!my_rl_rw_history)
return 0;
rc = my_rl_rw_history (filename, 0, nlines);
if (rc && gpg_err_code_from_syserror () == GPG_ERR_ENOENT)
rc = 0;
return rc;
}
/* Write the current history to the file FILENAME. Returns 0 on
* success or -1 on error and sets ERRNO. No error is return if
* readline support is not available. */
int
tty_write_history (const char *filename)
{
if (!my_rl_rw_history)
return 0;
return my_rl_rw_history (filename, 1, 0);
}
#ifdef HAVE_LIBREADLINE
void
tty_enable_completion (rl_completion_func_t *completer)
{
if (no_terminal || !my_rl_set_completer )
return;
if (!initialized)
init_ttyfp();
my_rl_set_completer (completer);
}
void
tty_disable_completion (void)
{
if (no_terminal || !my_rl_inhibit_completion)
return;
if (!initialized)
init_ttyfp();
my_rl_inhibit_completion (1);
}
#endif /* HAVE_LIBREADLINE */
void
tty_cleanup_after_signal (void)
{
#ifdef HAVE_TCGETATTR
cleanup ();
#endif
}
void
tty_cleanup_rl_after_signal (void)
{
if (my_rl_cleanup_after_signal)
my_rl_cleanup_after_signal ();
}