From c78385e0b25a791fa1f917b415fe846f891bcab6 Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Sat, 21 Jul 2018 20:15:57 +0000 Subject: [PATCH] #1646: add GUI dialog for confirming keys via hidden xpra subcommand git-svn-id: https://xpra.org/svn/Xpra/trunk@19941 3bb7dfac-3a0b-4e04-842a-767bc560f471 --- src/xpra/client/gtk_base/confirm_dialog.py | 190 +++++++++++++++++++++ src/xpra/scripts/main.py | 105 ++++++++---- 2 files changed, 264 insertions(+), 31 deletions(-) create mode 100755 src/xpra/client/gtk_base/confirm_dialog.py diff --git a/src/xpra/client/gtk_base/confirm_dialog.py b/src/xpra/client/gtk_base/confirm_dialog.py new file mode 100755 index 0000000000..dcd94f1231 --- /dev/null +++ b/src/xpra/client/gtk_base/confirm_dialog.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# This file is part of Xpra. +# Copyright (C) 2018 Antoine Martin +# Xpra is released under the terms of the GNU GPL v2, or, at your option, any +# later version. See the file COPYING for details. + + +import os.path +import sys +import signal + +from xpra.platform.gui import init as gui_init +gui_init() + +from xpra.gtk_common.gobject_compat import import_gtk, import_pango, import_glib + +gtk = import_gtk() +glib = import_glib() +pango = import_pango() + + +from xpra.os_util import get_util_logger +from xpra.gtk_common.gtk_util import gtk_main, add_close_accel, scaled_image, pixbuf_new_from_file, window_defaults, color_parse, \ + WIN_POS_CENTER, WINDOW_POPUP, STATE_NORMAL, is_gtk3 +from xpra.platform.paths import get_icon_dir +log = get_util_logger() + + +class ConfirmDialogWindow(object): + + def __init__(self, title="Title", prompt="", info=[], icon="", buttons=[]): + if is_gtk3(): + self.window = gtk.Window(type=WINDOW_POPUP) + else: + self.window = gtk.Window(WINDOW_POPUP) + window_defaults(self.window) + self.window.set_position(WIN_POS_CENTER) + self.window.connect("destroy", self.quit) + self.window.set_default_size(400, 150) + self.window.set_title(title) + #self.window.set_modal(True) + + if icon: + icon_pixbuf = self.get_icon(icon) + if icon_pixbuf: + self.window.set_icon(icon_pixbuf) + + vbox = gtk.VBox(False, 0) + vbox.set_spacing(10) + + def al(label, font="sans 14", xalign=0): + l = gtk.Label(label) + l.modify_font(pango.FontDescription(font)) + if label.startswith("WARNING"): + red = color_parse("red") + l.modify_fg(STATE_NORMAL, red) + al = gtk.Alignment(xalign=xalign, yalign=0.5, xscale=0.0, yscale=0) + al.add(l) + vbox.add(al) + + al(title, "sans 18", 0.5) + al(info, "sans 14") + al(prompt, "sans 14") + + # Buttons: + self.exit_code = 0 + if buttons: + hbox = gtk.HBox(False, 0) + al = gtk.Alignment(xalign=1, yalign=0.5, xscale=0, yscale=0) + al.add(hbox) + vbox.pack_start(al) + for label, code in buttons: + b = self.btn(label, "", code) + hbox.pack_start(b) + #btn("Close", "", self.close, "quit.png") + + add_close_accel(self.window, self.quit) + vbox.show_all() + self.window.add(vbox) + + def btn(self, label, tooltip, code, icon_name=None): + btn = gtk.Button(label) + settings = btn.get_settings() + settings.set_property('gtk-button-images', True) + if tooltip: + btn.set_tooltip_text(tooltip) + def btn_clicked(*_args): + log("%s button clicked, returning %s", label, code) + self.exit_code = code + self.quit() + btn.set_size_request(100, 48) + btn.connect("clicked", btn_clicked) + btn.set_can_focus(True) + isdefault = label[:1].upper()!=label[:1] + btn.set_can_default(isdefault) + if isdefault: + self.window.set_default(btn) + self.window.set_focus(btn) + if icon_name: + icon = self.get_icon(icon_name) + if icon: + btn.set_image(scaled_image(icon, 24)) + return btn + + + def show(self): + log("show()") + self.window.show_all() + glib.idle_add(self.window.present) + + def destroy(self, *args): + log("destroy%s", args) + if self.window: + self.window.destroy() + self.window = None + + def run(self): + log("run()") + gtk_main() + log("run() gtk_main done") + return self.exit_code + + def quit(self, *args): + log("quit%s", args) + self.destroy() + gtk.main_quit() + + + def get_icon(self, icon_name): + icon_filename = os.path.join(get_icon_dir(), icon_name) + if os.path.exists(icon_filename): + return pixbuf_new_from_file(icon_filename) + return None + + +def show_confirm_dialog(argv): + from xpra.os_util import SIGNAMES + from xpra.platform.gui import ready as gui_ready + from xpra.gtk_common.quit import gtk_main_quit_on_fatal_exceptions_enable + gtk_main_quit_on_fatal_exceptions_enable() + + log("show_confirm_dialog(%s)", argv) + def arg(n): + if len(argv)<=n: + return "" + return argv[n].replace("\\n\\r", "\\n").replace("\\n", "\n") + title = arg(0) or "Confirm Key" + prompt = arg(1) + info = arg(2) + icon = arg(3) + buttons = [] + n = 4 + while len(argv)>(n+1): + label = arg(n) + try: + code = int(arg(n+1)) + except ValueError as e: + log.error("Error: confirm dialog cannot parse code '%s': %s", arg(n+1), e) + return 1 + buttons.append((label, code)) + n += 2 + app = ConfirmDialogWindow(title, prompt, info, icon, buttons) + def app_signal(signum, _frame): + print("") + log.info("got signal %s", SIGNAMES.get(signum, signum)) + app.quit() + signal.signal(signal.SIGINT, app_signal) + signal.signal(signal.SIGTERM, app_signal) + gui_ready() + app.show() + return app.run() + + +def main(): + from xpra.platform import program_context + with program_context("Confirm-Dialog", "Confirm Dialog"): + #logging init: + if "-v" in sys.argv: + from xpra.log import enable_debug_for + enable_debug_for("util") + + try: + return show_confirm_dialog(sys.argv[1:]) + except KeyboardInterrupt: + return 1 + + +if __name__ == "__main__": + v = main() + sys.exit(v) diff --git a/src/xpra/scripts/main.py b/src/xpra/scripts/main.py index 274294e8a1..21a266c9ef 100755 --- a/src/xpra/scripts/main.py +++ b/src/xpra/scripts/main.py @@ -18,7 +18,7 @@ import traceback from xpra.platform.dotxpra import DotXpra -from xpra.util import csv, envbool, envint, engs, DEFAULT_PORT +from xpra.util import csv, envbool, envint, engs, nonl, DEFAULT_PORT from xpra.exit_codes import EXIT_SSL_FAILURE, EXIT_SSH_FAILURE, EXIT_SSH_KEY_FAILURE, EXIT_STR from xpra.os_util import get_util_logger, getuid, getgid, monotonic_time, setsid, bytestostr, osexpand, WIN32, OSX, POSIX from xpra.scripts.parsing import info, warn, error, \ @@ -28,9 +28,9 @@ from xpra.scripts.config import OPTION_TYPES, CLIENT_OPTIONS, NON_COMMAND_LINE_OPTIONS, CLIENT_ONLY_OPTIONS, START_COMMAND_OPTIONS, BIND_OPTIONS, PROXY_START_OVERRIDABLE_OPTIONS, OPTIONS_ADDED_SINCE_V1, \ InitException, InitInfo, InitExit, \ fixup_options, dict_to_validated_config, \ - make_defaults_struct, parse_bool, has_sound_support, name_to_field,\ - TRUE_OPTIONS + make_defaults_struct, parse_bool, has_sound_support, name_to_field from xpra.net.common import ConnectionClosedException +from xpra.platform.paths import get_xpra_command assert info and warn and error, "used by modules importing those from here" NO_ROOT_WARNING = envbool("XPRA_NO_ROOT_WARNING", False) @@ -132,7 +132,7 @@ def configure_logging(options, mode): #the logging system every time, and just undo things here.. from xpra.log import setloghandler, enable_color, enable_format, LOG_FORMAT, NOPREFIX_FORMAT setloghandler(logging.StreamHandler(to)) - if mode in ("start", "start-desktop", "upgrade", "attach", "shadow", "proxy", "_sound_record", "_sound_play", "stop", "print", "showconfig", "request-start", "request-start-desktop", "request-shadow"): + if mode in ("start", "start-desktop", "upgrade", "attach", "shadow", "proxy", "_sound_record", "_sound_play", "stop", "print", "showconfig", "request-start", "request-start-desktop", "request-shadow", "_dialog"): if "help" in options.speaker_codec or "help" in options.microphone_codec: info = show_sound_codec_help(mode!="attach", options.speaker_codec, options.microphone_codec) raise InitInfo("\n".join(info)) @@ -400,6 +400,8 @@ def attach_client(): error_cb("no sound support!") from xpra.sound.wrapper import run_sound return run_sound(mode, error_cb, options, args) + elif mode in ("_dialog"): + return run_dialog(args) elif mode=="opengl": return run_glcheck(options) elif mode == "initenv": @@ -1017,15 +1019,47 @@ def keymd5(k): f = f[2:] return s -def confirm_key(): +def dialog_confirm(title, prompt, qinfo="", icon="", buttons=[("OK", 1)]): + cmd = get_xpra_command()+["_dialog", nonl(title), nonl(prompt), nonl("\\n".join(qinfo)), icon] + for label, code in buttons: + cmd.append(nonl(label)) + cmd.append(str(code)) + import subprocess + env = os.environ.copy() + log = get_util_logger() + try: + log("dialog_confirm command: %s", cmd) + proc = subprocess.Popen(cmd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, env=env) + stdout, stderr = proc.communicate() + if stderr: + log.warn("Warning: dialog process error output:") + for x in stderr.splitlines(): + log.warn(" %s", x) + return proc.returncode, stdout + except Exception as e: + log("dialog_confirm(..)", exc_info=True) + log.error("Error: failed to execute the dialog subcommand") + log.error(" %s", e) + return -1, "" + +def confirm_key(info=[]): SKIP_UI = envbool("XPRA_SKIP_UI", False) if SKIP_UI: return False + from xpra.platform.paths import get_icon_filename from xpra.os_util import use_tty if not use_tty(): - #TODO! - return False - v = sys.stdin.readline().rstrip("\n\r") + icon = get_icon_filename("authentication", "png") + prompt = "Are you sure you want to continue connecting?" + code, out = dialog_confirm("Confirm Key", prompt, info, icon, buttons=[("yes", 200), ("NO", 201)]) + log = get_util_logger() + log.debug("dialog output: '%s', return code=%s", nonl(out), code) + r = code==200 + log.info("host key %sconfirmed", ["not ", ""][r]) + return r + prompt = "Are you sure you want to continue connecting (yes/NO)?" + sys.stderr.write(os.linesep.join(info)+os.linesep+prompt) + v = sys.stdin.readline().rstrip(os.linesep) return v and v.lower() in ("y", "yes") def input_pass(prompt): @@ -1093,39 +1127,42 @@ def keyname(): else: if known_host_key: log.warn("Warning: SSH server key mismatch") - sys.stderr.write( -"""@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n -@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @\n -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n -IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n -Someone could be eavesdropping on you right now (man-in-the-middle attack)!\n -It is also possible that a host key has just been changed.\n -The fingerprint for the %s key sent by the remote host is\n -%s\n -""" % (keyname(), keymd5(host_key))) + qinfo = [ +"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!", +"IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!", +"Someone could be eavesdropping on you right now (man-in-the-middle attack)!", +"It is also possible that a host key has just been changed.", +"The fingerprint for the %s key sent by the remote host is" % keyname(), +keymd5(host_key), +] if envbool("XPRA_SSH_VERIFY_STRICT", False): - log.warn("Please contact your system administrator.") - log.warn("Add correct host key in %s to get rid of this message.") - log.warn("Offending %s key in %s", keyname(), host_keys_filename) - log.warn("ECDSA host key for %s has changed and you have requested strict checking.", keyname()) log.warn("Host key verification failed.") + #TODO: show alert with no option to accept key + qinfo += [ + "Please contact your system administrator.", + "Add correct host key in %s to get rid of this message.", + "Offending %s key in %s" % (keyname(), host_keys_filename), + "ECDSA host key for %s has changed and you have requested strict checking." % keyname(), + ] + sys.stderr.write(os.linesep.join(qinfo)) transport.close() raise InitExit(EXIT_SSH_KEY_FAILURE, "SSH Host key has changed") - sys.stderr.write("Are you sure you want to continue connecting (yes/no)?") - if not confirm_key(): + if not confirm_key(qinfo): transport.close() raise InitExit(EXIT_SSH_KEY_FAILURE, "SSH Host key has changed") else: assert (not keys) or (host_key.get_name() not in keys) if not keys: - log.warn("Warning: unknown host") + log.warn("Warning: unknown SSH host") else: - log.warn("Warning: unknown %s host key", keyname()) - sys.stderr.write("The authenticity of host '%s' can't be established.\n" % (host,)) - sys.stderr.write("%s key fingerprint is %s\n" % (keyname(), keymd5(host_key))) - sys.stderr.write("Are you sure you want to continue connecting (yes/no)?") - if not confirm_key(): + log.warn("Warning: unknown %s SSH host key", keyname()) + qinfo = [ + "The authenticity of host '%s' can't be established." % (host,), + "%s key fingerprint is" % keyname(), + keymd5(host_key), + ] + if not confirm_key(qinfo): transport.close() raise InitExit(EXIT_SSH_KEY_FAILURE, "Unknown SSH host '%s'" % host) @@ -1596,6 +1633,11 @@ def do_wrap_socket(tcp_socket): return do_wrap_socket +def run_dialog(extra_args): + from xpra.client.gtk_base.confirm_dialog import show_confirm_dialog + return show_confirm_dialog(extra_args) + + def get_sockpath(display_desc, error_cb): #if the path was specified, use that: sockpath = display_desc.get("socket_path") @@ -1780,10 +1822,11 @@ def impcheck(*modules): for mod in modules: try: __import__("xpra.%s" % mod, {}, {}, []) - except ImportError as e: + except ImportError: if mod not in impwarned: impwarned.append(mod) log = get_util_logger() + log("impcheck%s", modules, exc_info=True) log.warn("Warning: missing %s module", mod) return False return True