From 3b0947c9efd6266bc8ce8738f1050ad0b6ea7582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Sun, 14 Nov 2021 18:40:26 +0100 Subject: [PATCH] Add support of TOTP and password hash (WeeChat >= 2.9) --- README.md | 1 + qweechat/connection.py | 94 ++++++++--- qweechat/data/icons/README | 5 +- qweechat/data/icons/dialog-password.png | Bin 0 -> 713 bytes qweechat/network.py | 197 ++++++++++++++++++++---- qweechat/preferences.py | 57 +++++++ qweechat/qweechat.py | 170 ++++++++++---------- 7 files changed, 389 insertions(+), 135 deletions(-) create mode 100644 qweechat/data/icons/dialog-password.png create mode 100644 qweechat/preferences.py diff --git a/README.md b/README.md index b836ba3..9942a91 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ In QWeeChat, click on connect and enter fields: - `server`: the IP address or hostname of your machine with WeeChat running - `port`: the relay port (defined in WeeChat) - `password`: the relay password (defined in WeeChat) +- `totp`: the Time-Based One-Time Password (optional, to set if required by WeeChat) Options can be changed in file `~/.config/qweechat/qweechat.conf`. diff --git a/qweechat/connection.py b/qweechat/connection.py index 9a5adba..aad350a 100644 --- a/qweechat/connection.py +++ b/qweechat/connection.py @@ -32,35 +32,91 @@ class ConnectionDialog(QtWidgets.QDialog): super().__init__(*args) self.values = values self.setModal(True) + self.setWindowTitle('Connect to WeeChat') grid = QtWidgets.QGridLayout() grid.setSpacing(10) self.fields = {} - for line, field in enumerate(('server', 'port', 'password', 'lines')): - grid.addWidget(QtWidgets.QLabel(field.capitalize()), line, 0) - line_edit = QtWidgets.QLineEdit() - line_edit.setFixedWidth(200) - if field == 'password': - line_edit.setEchoMode(QtWidgets.QLineEdit.Password) - if field == 'lines': - validator = QtGui.QIntValidator(0, 2147483647, self) - line_edit.setValidator(validator) - line_edit.setFixedWidth(80) - line_edit.insert(self.values[field]) - grid.addWidget(line_edit, line, 1) - self.fields[field] = line_edit - if field == 'port': - ssl = QtWidgets.QCheckBox('SSL') - ssl.setChecked(self.values['ssl'] == 'on') - grid.addWidget(ssl, line, 2) - self.fields['ssl'] = ssl + focus = None + + # server + grid.addWidget(QtWidgets.QLabel('Server'), 0, 0) + line_edit = QtWidgets.QLineEdit() + line_edit.setFixedWidth(200) + value = self.values.get('server', '') + line_edit.insert(value) + grid.addWidget(line_edit, 0, 1) + self.fields['server'] = line_edit + if not focus and not value: + focus = 'server' + + # port / SSL + grid.addWidget(QtWidgets.QLabel('Port'), 1, 0) + line_edit = QtWidgets.QLineEdit() + line_edit.setFixedWidth(200) + value = self.values.get('port', '') + line_edit.insert(value) + grid.addWidget(line_edit, 1, 1) + self.fields['port'] = line_edit + if not focus and not value: + focus = 'port' + + ssl = QtWidgets.QCheckBox('SSL') + ssl.setChecked(self.values['ssl'] == 'on') + grid.addWidget(ssl, 1, 2) + self.fields['ssl'] = ssl + + # password + grid.addWidget(QtWidgets.QLabel('Password'), 2, 0) + line_edit = QtWidgets.QLineEdit() + line_edit.setFixedWidth(200) + line_edit.setEchoMode(QtWidgets.QLineEdit.Password) + value = self.values.get('password', '') + line_edit.insert(value) + grid.addWidget(line_edit, 2, 1) + self.fields['password'] = line_edit + if not focus and not value: + focus = 'password' + + # TOTP (Time-Based One-Time Password) + label = QtWidgets.QLabel('TOTP') + label.setToolTip('Time-Based One-Time Password (6 digits)') + grid.addWidget(label, 3, 0) + line_edit = QtWidgets.QLineEdit() + line_edit.setPlaceholderText('6 digits') + validator = QtGui.QIntValidator(0, 999999, self) + line_edit.setValidator(validator) + line_edit.setFixedWidth(80) + value = self.values.get('totp', '') + line_edit.insert(value) + grid.addWidget(line_edit, 3, 1) + self.fields['totp'] = line_edit + if not focus and not value: + focus = 'totp' + + # lines + grid.addWidget(QtWidgets.QLabel('Lines'), 4, 0) + line_edit = QtWidgets.QLineEdit() + line_edit.setFixedWidth(200) + validator = QtGui.QIntValidator(0, 2147483647, self) + line_edit.setValidator(validator) + line_edit.setFixedWidth(80) + value = self.values.get('lines', '') + line_edit.insert(value) + grid.addWidget(line_edit, 4, 1) + self.fields['lines'] = line_edit + if not focus and not value: + focus = 'lines' self.dialog_buttons = QtWidgets.QDialogButtonBox() self.dialog_buttons.setStandardButtons( QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) self.dialog_buttons.rejected.connect(self.close) - grid.addWidget(self.dialog_buttons, 4, 0, 1, 2) + grid.addWidget(self.dialog_buttons, 5, 0, 1, 2) self.setLayout(grid) self.show() + + if focus: + self.fields[focus].setFocus() diff --git a/qweechat/data/icons/README b/qweechat/data/icons/README index 614a9d9..42c617b 100644 --- a/qweechat/data/icons/README +++ b/qweechat/data/icons/README @@ -10,8 +10,9 @@ Files: weechat.png, bullet_green_8x8.png, bullet_yellow_8x8.png Files: application-exit.png, dialog-close.png, dialog-ok-apply.png, - dialog-warning.png, document-save.png, edit-find.png, help-about.png, - network-connect.png, network-disconnect.png, preferences-other.png + dialog-password.png, dialog-warning.png, document-save.png, + edit-find.png, help-about.png, network-connect.png, + network-disconnect.png, preferences-other.png Files come from Debian package "oxygen-icon-theme": diff --git a/qweechat/data/icons/dialog-password.png b/qweechat/data/icons/dialog-password.png new file mode 100644 index 0000000000000000000000000000000000000000..2151029e0adbab6678e541f1ab0a080809f0a44f GIT binary patch literal 713 zcmV;)0yh1LP)XV#?~IOCxKK)w2oo1A+@;VeaPbyut7zx0 zg*$g461>2|wLx!D3q#AcA=E~F2t5(Mz=a5YE2cDliyv6lsB_=B(md)>FZ;t`KJNU^ z{hxEsAdKOQPitDR^x+AtUl5B>+fnMfq6tJR9n=ks_l7~o(q*rU;&vfJ$s zjYi|GQmI6#R4UZzbdXxDmh7{$()Zf;j%C?ZHk-w*RtuBsa=FBTK;ZMg;OpUVxG|Yb za4MC;^?Dtr(`md|EJ%_ca=HA`d4Peq3UR|?wcK;LT(5e)9v_d#ak*T^$z&2wr&FBC zWH^9b@ZA-PfD}cE%x3d_o6Yt_r_*VqQmKUPvW#A@m$F}2q*N-=d_G6;j2evwXR%n^ zkmv~5ZwN#?oz5dz*pkkU$K$gomWue~O*Zo*5_7Uzke@xKr zcHhI0wASl2A`)B>k#~N-zYC?kMSqbnKzF=eZw2O@C!= 0: + text = '(debug_%s)%s' % (text[1:pos], text[pos+1:]) + else: + text = '(debug) %s' % text + self.network.debug_print(0, '<==', text, forcecolor='#AA0000') + self.network.send_to_weechat(text + '\n') + + def open_debug_dialog(self): + """Open a dialog with debug messages.""" + if not self.debug_dialog: + self.debug_dialog = DebugDialog() + self.debug_dialog.input.textSent.connect( + self.debug_input_text_sent) + self.debug_dialog.finished.connect(self._debug_dialog_closed) + self.debug_dialog.display_lines(self.debug_lines) + self.debug_dialog.chat.scroll_bottom() diff --git a/qweechat/preferences.py b/qweechat/preferences.py new file mode 100644 index 0000000..e8c276f --- /dev/null +++ b/qweechat/preferences.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# preferences.py - preferences dialog box +# +# Copyright (C) 2011-2021 Sébastien Helleu +# +# This file is part of QWeeChat, a Qt remote GUI for WeeChat. +# +# QWeeChat is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# QWeeChat 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 QWeeChat. If not, see . +# + +"""Preferences dialog box.""" + +from PySide6 import QtCore, QtWidgets as QtGui + + +class PreferencesDialog(QtGui.QDialog): + """Preferences dialog.""" + + def __init__(self, *args): + QtGui.QDialog.__init__(*(self,) + args) + self.setModal(True) + self.setWindowTitle('Preferences') + + close_button = QtGui.QPushButton('Close') + close_button.pressed.connect(self.close) + + hbox = QtGui.QHBoxLayout() + hbox.addStretch(1) + hbox.addWidget(close_button) + hbox.addStretch(1) + + vbox = QtGui.QVBoxLayout() + + label = QtGui.QLabel('Not yet implemented!') + label.setAlignment(QtCore.Qt.AlignHCenter) + vbox.addWidget(label) + + label = QtGui.QLabel('') + label.setAlignment(QtCore.Qt.AlignHCenter) + vbox.addWidget(label) + + vbox.addLayout(hbox) + + self.setLayout(vbox) + self.show() diff --git a/qweechat/qweechat.py b/qweechat/qweechat.py index 1888d6d..66a8d6a 100644 --- a/qweechat/qweechat.py +++ b/qweechat/qweechat.py @@ -40,21 +40,18 @@ from pkg_resources import resource_filename from PySide6 import QtCore, QtGui, QtWidgets from qweechat import config -from qweechat.weechat import protocol -from qweechat.network import Network, STATUS_DISCONNECTED, NETWORK_STATUS -from qweechat.connection import ConnectionDialog -from qweechat.buffer import BufferListWidget, Buffer -from qweechat.debug import DebugDialog from qweechat.about import AboutDialog +from qweechat.buffer import BufferListWidget, Buffer +from qweechat.connection import ConnectionDialog +from qweechat.network import Network, STATUS_DISCONNECTED, NETWORK_STATUS +from qweechat.preferences import PreferencesDialog +from qweechat.weechat import protocol APP_NAME = 'QWeeChat' AUTHOR = 'Sébastien Helleu' WEECHAT_SITE = 'https://weechat.org/' -# number of lines in buffer for debug window -DEBUG_NUM_LINES = 50 - class MainWindow(QtWidgets.QMainWindow): """Main window.""" @@ -67,9 +64,6 @@ class MainWindow(QtWidgets.QMainWindow): self.resize(1000, 600) self.setWindowTitle(APP_NAME) - self.debug_dialog = None - self.debug_lines = [] - self.about_dialog = None self.connection_dialog = None self.preferences_dialog = None @@ -101,26 +95,47 @@ class MainWindow(QtWidgets.QMainWindow): # actions for menu and toolbar actions_def = { 'connect': [ - 'network-connect.png', 'Connect to WeeChat', - 'Ctrl+O', self.open_connection_dialog], + 'network-connect.png', + 'Connect to WeeChat', + 'Ctrl+O', + self.open_connection_dialog, + ], 'disconnect': [ - 'network-disconnect.png', 'Disconnect from WeeChat', - 'Ctrl+D', self.network.disconnect_weechat], + 'network-disconnect.png', + 'Disconnect from WeeChat', + 'Ctrl+D', + self.network.disconnect_weechat, + ], 'debug': [ - 'edit-find.png', 'Debug console window', - 'Ctrl+B', self.open_debug_dialog], + 'edit-find.png', + 'Open debug console window', + 'Ctrl+B', + self.network.open_debug_dialog, + ], 'preferences': [ - 'preferences-other.png', 'Preferences', - 'Ctrl+P', self.open_preferences_dialog], + 'preferences-other.png', + 'Change preferences', + 'Ctrl+P', + self.open_preferences_dialog, + ], 'about': [ - 'help-about.png', 'About', - 'Ctrl+H', self.open_about_dialog], + 'help-about.png', + 'About QWeeChat', + 'Ctrl+H', + self.open_about_dialog, + ], 'save connection': [ - 'document-save.png', 'Save connection configuration', - 'Ctrl+S', self.save_connection], + 'document-save.png', + 'Save connection configuration', + 'Ctrl+S', + self.save_connection, + ], 'quit': [ - 'application-exit.png', 'Quit application', - 'Ctrl+Q', self.close], + 'application-exit.png', + 'Quit application', + 'Ctrl+Q', + self.close, + ], } self.actions = {} for name, action in list(actions_def.items()): @@ -128,7 +143,7 @@ class MainWindow(QtWidgets.QMainWindow): QtGui.QIcon( resource_filename(__name__, 'data/icons/%s' % action[0])), name.capitalize(), self) - self.actions[name].setStatusTip(action[1]) + self.actions[name].setToolTip(f'{action[1]} ({action[2]})') self.actions[name].setShortcut(action[2]) self.actions[name].triggered.connect(action[3]) @@ -168,16 +183,18 @@ class MainWindow(QtWidgets.QMainWindow): # open debug dialog if self.config.getboolean('look', 'debug'): - self.open_debug_dialog() + self.network.open_debug_dialog() # auto-connect to relay if self.config.getboolean('relay', 'autoconnect'): - self.network.connect_weechat(self.config.get('relay', 'server'), - self.config.get('relay', 'port'), - self.config.getboolean('relay', - 'ssl'), - self.config.get('relay', 'password'), - self.config.get('relay', 'lines')) + self.network.connect_weechat( + server=self.config.get('relay', 'server'), + port=self.config.get('relay', 'port'), + ssl=self.config.getboolean('relay', 'ssl'), + password=self.config.get('relay', 'password'), + totp=None, + lines=self.config.get('relay', 'lines'), + ) self.show() @@ -192,14 +209,12 @@ class MainWindow(QtWidgets.QMainWindow): if self.network.is_connected(): message = 'input %s %s\n' % (full_name, text) self.network.send_to_weechat(message) - self.debug_display(0, '<==', message, forcecolor='#AA0000') + self.network.debug_print(0, '<==', message, forcecolor='#AA0000') def open_preferences_dialog(self): """Open a dialog with preferences.""" # TODO: implement the preferences dialog box - messages = ['Not yet implemented!', - ''] - self.preferences_dialog = AboutDialog('Preferences', messages, self) + self.preferences_dialog = PreferencesDialog(self) def save_connection(self): """Save connection configuration.""" @@ -208,39 +223,6 @@ class MainWindow(QtWidgets.QMainWindow): for option in options: self.config.set('relay', option, options[option]) - def debug_display(self, *args, **kwargs): - """Display a debug message.""" - self.debug_lines.append((args, kwargs)) - self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:] - if self.debug_dialog: - self.debug_dialog.chat.display(*args, **kwargs) - - def open_debug_dialog(self): - """Open a dialog with debug messages.""" - if not self.debug_dialog: - self.debug_dialog = DebugDialog(self) - self.debug_dialog.input.textSent.connect( - self.debug_input_text_sent) - self.debug_dialog.finished.connect(self._debug_dialog_closed) - self.debug_dialog.display_lines(self.debug_lines) - self.debug_dialog.chat.scroll_bottom() - - def debug_input_text_sent(self, text): - """Send debug buffer input to WeeChat.""" - if self.network.is_connected(): - text = str(text) - pos = text.find(')') - if text.startswith('(') and pos >= 0: - text = '(debug_%s)%s' % (text[1:pos], text[pos+1:]) - else: - text = '(debug) %s' % text - self.debug_display(0, '<==', text, forcecolor='#AA0000') - self.network.send_to_weechat(text + '\n') - - def _debug_dialog_closed(self, result): - """Called when debug dialog is closed.""" - self.debug_dialog = None - def open_about_dialog(self): """Open a dialog with info about QWeeChat.""" self.about_dialog = AboutDialog(APP_NAME, AUTHOR, WEECHAT_SITE, self) @@ -257,18 +239,20 @@ class MainWindow(QtWidgets.QMainWindow): def connect_weechat(self): """Connect to WeeChat.""" self.network.connect_weechat( - self.connection_dialog.fields['server'].text(), - self.connection_dialog.fields['port'].text(), - self.connection_dialog.fields['ssl'].isChecked(), - self.connection_dialog.fields['password'].text(), - int(self.connection_dialog.fields['lines'].text())) + server=self.connection_dialog.fields['server'].text(), + port=self.connection_dialog.fields['port'].text(), + ssl=self.connection_dialog.fields['ssl'].isChecked(), + password=self.connection_dialog.fields['password'].text(), + totp=self.connection_dialog.fields['totp'].text(), + lines=int(self.connection_dialog.fields['lines'].text()), + ) self.connection_dialog.close() def _network_status_changed(self, status, extra): """Called when the network status has changed.""" if self.config.getboolean('look', 'statusbar'): self.statusBar().showMessage(status) - self.debug_display(0, '', status, forcecolor='#0000AA') + self.network.debug_print(0, '', status, forcecolor='#0000AA') self.network_status_set(status) def network_status_set(self, status): @@ -296,30 +280,40 @@ class MainWindow(QtWidgets.QMainWindow): def _network_weechat_msg(self, message): """Called when a message is received from WeeChat.""" - self.debug_display(0, '==>', - 'message (%d bytes):\n%s' - % (len(message), - protocol.hex_and_ascii(message.data(), 20)), - forcecolor='#008800') + self.network.debug_print( + 0, '==>', + 'message (%d bytes):\n%s' + % (len(message), + protocol.hex_and_ascii(message.data(), 20)), + forcecolor='#008800', + ) try: proto = protocol.Protocol() message = proto.decode(message.data()) if message.uncompressed: - self.debug_display( + self.network.debug_print( 0, '==>', 'message uncompressed (%d bytes):\n%s' % (message.size_uncompressed, protocol.hex_and_ascii(message.uncompressed, 20)), forcecolor='#008800') - self.debug_display(0, '', 'Message: %s' % message) + self.network.debug_print(0, '', 'Message: %s' % message) self.parse_message(message) except Exception: # noqa: E722 print('Error while decoding message from WeeChat:\n%s' % traceback.format_exc()) self.network.disconnect_weechat() + def _parse_handshake(self, message): + """Parse a WeeChat message with handshake response.""" + for obj in message.objects: + if obj.objtype != 'htb': + continue + self.network.init_with_handshake(obj.value) + break + def _parse_listbuffers(self, message): - """Parse a WeeChat with list of buffers.""" + """Parse a WeeChat message with list of buffers.""" for obj in message.objects: if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer': continue @@ -462,7 +456,9 @@ class MainWindow(QtWidgets.QMainWindow): def parse_message(self, message): """Parse a WeeChat message.""" if message.msgid.startswith('debug'): - self.debug_display(0, '', '(debug message, ignored)') + self.network.debug_print(0, '', '(debug message, ignored)') + elif message.msgid == 'handshake': + self._parse_handshake(message) elif message.msgid == 'listbuffers': self._parse_listbuffers(message) elif message.msgid in ('listlines', '_buffer_line_added'): @@ -526,8 +522,8 @@ class MainWindow(QtWidgets.QMainWindow): def closeEvent(self, event): """Called when QWeeChat window is closed.""" self.network.disconnect_weechat() - if self.debug_dialog: - self.debug_dialog.close() + if self.network.debug_dialog: + self.network.debug_dialog.close() config.write(self.config) QtWidgets.QMainWindow.closeEvent(self, event)