From b15a890eef1d3d72e952fdc146ec57c89010fdbf Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Wed, 14 Jan 2015 23:07:48 +0200 Subject: [PATCH 1/5] initial OpenAlias plugin commit --- plugins/openalias.py | 259 +++++++++++++++++++++++++++++++++++++++++++ setup.py | 3 +- 2 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 plugins/openalias.py diff --git a/plugins/openalias.py b/plugins/openalias.py new file mode 100644 index 000000000..3a80da62b --- /dev/null +++ b/plugins/openalias.py @@ -0,0 +1,259 @@ +from electrum_gui.qt.util import EnterButton +from electrum.plugins import BasePlugin, hook +from electrum.i18n import _ +from PyQt4.QtGui import * +from PyQt4.QtCore import * + +import re + +import dns.name +import dns.query +import dns.dnssec +import dns.message +import dns.resolver +import dns.rdatatype +from dns.exception import DNSException + + +class Plugin(BasePlugin): + def fullname(self): + return 'OpenAlias' + + def description(self): + return 'Import contacts by OpenAlias.' + + def __init__(self, gui, name): + BasePlugin.__init__(self, gui, name) + self._is_available = True + + @hook + def init_qt(self, gui): + self.gui = gui + self.win = gui.main_window + + def requires_settings(self): + return True + + def settings_widget(self, window): + return EnterButton(_('Settings'), self.settings_dialog) + + @hook + def before_send(self): + ''' + Change URL to address before making a send. + IMPORTANT: + return False to continue execution of the send + return True to stop execution of the send + ''' + if self.win.payto_e.is_multiline(): # only supports single line entries atm + return False + url = str(self.win.payto_e.toPlainText()) + + if not '.' in url: + return False + + data = self.resolve(url) + + if not data: + return True + + if not self.validate_dnssec(url): + msgBox = QMessageBox() + msgBox.setText(_('No valid DNSSEC trust chain!')) + msgBox.setInformativeText(_('Do you wish to continue?')) + msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) + msgBox.setDefaultButton(QMessageBox.Cancel) + reply = msgBox.exec_() + if reply != QMessageBox.Ok: + return True + + (address, name) = data + self.win.payto_e.setText(address) + if self.config.get('openalias_autoadd') == 'checked': + self.win.wallet.add_contact(address, name) + return False + + def settings_dialog(self): + '''Settings dialog.''' + d = QDialog() + d.setWindowTitle("Settings") + layout = QGridLayout(d) + layout.addWidget(QLabel(_('Automatically add to contacts')), 0, 0) + autoadd_checkbox = QCheckBox() + autoadd_checkbox.setEnabled(True) + autoadd_checkbox.setChecked(self.config.get('openalias_autoadd', 'unchecked') != 'unchecked') + layout.addWidget(autoadd_checkbox, 0, 1) + ok_button = QPushButton(_("OK")) + ok_button.clicked.connect(d.accept) + layout.addWidget(ok_button, 1, 1) + + def on_change_autoadd(checked): + if checked: + self.config.set_key('openalias_autoadd', 'checked') + else: + self.config.set_key('openalias_autoadd', 'unchecked') + + autoadd_checkbox.stateChanged.connect(on_change_autoadd) + + return bool(d.exec_()) + + def openalias_contact_dialog(self): + '''Previous version using a get contact button from settings, currently unused.''' + d = QDialog(self.win) + vbox = QVBoxLayout(d) + vbox.addWidget(QLabel(_('Openalias Contact') + ':')) + + grid = QGridLayout() + line1 = QLineEdit() + grid.addWidget(QLabel(_("URL")), 1, 0) + grid.addWidget(line1, 1, 1) + + vbox.addLayout(grid) + vbox.addLayout(ok_cancel_buttons(d)) + + if not d.exec_(): + return + + url = str(line1.text()) + + if not '.' in url: + QMessageBox.warning(self.win, _('Error'), _('Invalid URL'), _('OK')) + return + + data = self.resolve(url) + + if not data: + return + + if not self.validate_dnssec(url): + msgBox = QMessageBox() + msgBox.setText("No valid DNSSEC trust chain!") + msgBox.setInformativeText("Do you wish to continue?") + msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) + msgBox.setDefaultButton(QMessageBox.Cancel) + reply = msgBox.exec_() + if reply != QMessageBox.Ok: + return + + (address, name) = data + + d2 = QDialog(self.win) + vbox2 = QVBoxLayout(d2) + grid2 = QGridLayout() + grid2.addWidget(QLabel(url), 1, 1) + if name: + grid2.addWidget(QLabel('Name: '), 2, 0) + grid2.addWidget(QLabel(name), 2, 1) + + grid2.addWidget(QLabel('Address: '), 4, 0) + grid2.addWidget(QLabel(address), 4, 1) + + vbox2.addLayout(grid2) + vbox2.addLayout(ok_cancel_buttons(d2)) + + if not d2.exec_(): + return + + self.win.wallet.add_contact(address) + + try: + label = url + " (" + name + ")" + except Exception: + pass + + if label: + self.win.wallet.set_label(address, label) + + self.win.update_contacts_tab() + self.win.update_history_tab() + self.win.update_completions() + self.win.tabs.setCurrentIndex(3) + + def resolve(self, url): + '''Resolve OpenAlias address using url.''' + prefix = 'btc' + retries = 3 + err = None + for i in range(0, retries): + try: + records = dns.resolver.query(url, 'TXT') + for record in records: + string = record.strings[0] + if string.startswith('oa1:' + prefix): + address = self.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') + name = self.find_regex(string, r'recipient_name=([^;]+)') + if not address: + continue + return (address, name) + QMessageBox.warning(self.win, _('Error'), _('No OpenAlias record found.'), _('OK')) + return 0 + except dns.resolver.NXDOMAIN: + err = _('No such domain.') + continue + except dns.resolver.Timeout: + err = _('Timed out while resolving.') + continue + except DNSException: + err = _('Unhandled exception.') + continue + except: + err = _('Unknown error.') + continue + break + if err: + QMessageBox.warning(self.win, _('Error'), err, _('OK')) + return 0 + + def find_regex(self, haystack, needle): + regex = re.compile(needle) + try: + return regex.search(haystack).groups()[0] + except AttributeError: + return None + + def validate_dnssec(self, url): + default = dns.resolver.get_default_resolver() + ns = default.nameservers[0] + + parts = url.split('.') + + for i in xrange(len(parts), 0, -1): + sub = '.'.join(parts[i - 1:]) + + query = dns.message.make_query(sub, dns.rdatatype.NS) + response = dns.query.udp(query, ns) + + if response.rcode() != dns.rcode.NOERROR: + return 0 + + if len(response.authority) > 0: + rrset = response.authority[0] + else: + rrset = response.answer[0] + + rr = rrset[0] + if rr.rdtype == dns.rdatatype.SOA: + #Same server is authoritative, don't check again + continue + + query = dns.message.make_query(sub, + dns.rdatatype.DNSKEY, + want_dnssec=True) + response = dns.query.udp(query, ns) + + if response.rcode() != 0: + return 0 + # HANDLE QUERY FAILED (SERVER ERROR OR NO DNSKEY RECORD) + + # answer should contain two RRSET: DNSKEY and RRSIG(DNSKEY) + answer = response.answer + if len(answer) != 2: + return 0 + + # the DNSKEY should be self signed, validate it + name = dns.name.from_text(sub) + try: + dns.dnssec.validate(answer[0], answer[1], {name: answer[0]}) + except dns.dnssec.ValidationFailure: + return 0 + return 1 \ No newline at end of file diff --git a/setup.py b/setup.py index f3beb76fa..e728f5c5d 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,8 @@ setup( 'pyasn1-modules', 'qrcode', 'SocksiPy-branch', - 'tlslite' + 'tlslite', + 'pythondns' ], package_dir={ 'electrum': 'lib', From 1319f5827655f652ac63450e6deaf9a8155cb11f Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Thu, 15 Jan 2015 21:07:08 +0200 Subject: [PATCH 2/5] fixed incorrect dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e728f5c5d..1d6eca546 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( 'qrcode', 'SocksiPy-branch', 'tlslite', - 'pythondns' + 'dnspython' ], package_dir={ 'electrum': 'lib', From d14a4737b6bcf0c6053bc49735e7814941c3fd75 Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Sun, 18 Jan 2015 13:48:28 +0200 Subject: [PATCH 3/5] display error details for unexpected errors --- plugins/openalias.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index 3a80da62b..bdaae463b 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -196,8 +196,8 @@ class Plugin(BasePlugin): except DNSException: err = _('Unhandled exception.') continue - except: - err = _('Unknown error.') + except Exception,e: + err = _('Unexpected error: ' + str(e)) continue break if err: From 8b2af48b56cc6bf284463dcfdc41ca654144ea7d Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Sun, 18 Jan 2015 14:18:28 +0200 Subject: [PATCH 4/5] handle DNSPython libs not being available --- plugins/openalias.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index bdaae463b..0143f6b80 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -6,13 +6,17 @@ from PyQt4.QtCore import * import re -import dns.name -import dns.query -import dns.dnssec -import dns.message -import dns.resolver -import dns.rdatatype -from dns.exception import DNSException +try: + import dns.name + import dns.query + import dns.dnssec + import dns.message + import dns.resolver + import dns.rdatatype + from dns.exception import DNSException + OA_READY = True +except ImportError: + OA_READY = False class Plugin(BasePlugin): @@ -20,11 +24,14 @@ class Plugin(BasePlugin): return 'OpenAlias' def description(self): - return 'Import contacts by OpenAlias.' + return 'Allow for payments to OpenAlias addresses.' + + def is_available(self): + return OA_READY def __init__(self, gui, name): BasePlugin.__init__(self, gui, name) - self._is_available = True + self._is_available = OA_READY @hook def init_qt(self, gui): @@ -45,12 +52,17 @@ class Plugin(BasePlugin): return False to continue execution of the send return True to stop execution of the send ''' + if self.win.payto_e.is_multiline(): # only supports single line entries atm return False url = str(self.win.payto_e.toPlainText()) if not '.' in url: return False + else: + if not OA_READY: + QMessageBox.warning(self.win, _('Error'), 'Could not load DNSPython libraries, please ensure they are available and/or Electrum has been built correctly', _('OK')) + return False data = self.resolve(url) From 018c458be9f7486ce690ae57b3203c94b8b73e85 Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Mon, 19 Jan 2015 16:49:46 +0200 Subject: [PATCH 5/5] query timeouts, import relevant types --- plugins/openalias.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index 0143f6b80..545f789bc 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -13,6 +13,19 @@ try: import dns.message import dns.resolver import dns.rdatatype + import dns.rdtypes.ANY.NS + import dns.rdtypes.ANY.CNAME + import dns.rdtypes.ANY.DLV + import dns.rdtypes.ANY.DNSKEY + import dns.rdtypes.ANY.DS + import dns.rdtypes.ANY.NSEC + import dns.rdtypes.ANY.NSEC3 + import dns.rdtypes.ANY.NSEC3PARAM + import dns.rdtypes.ANY.RRSIG + import dns.rdtypes.ANY.SOA + import dns.rdtypes.ANY.TXT + import dns.rdtypes.IN.A + import dns.rdtypes.IN.AAAA from dns.exception import DNSException OA_READY = True except ImportError: @@ -188,7 +201,10 @@ class Plugin(BasePlugin): err = None for i in range(0, retries): try: - records = dns.resolver.query(url, 'TXT') + resolver = dns.resolver.Resolver() + resolver.timeout = 15.0 + resolver.lifetime = 15.0 + records = resolver.query(url, 'TXT') for record in records: string = record.strings[0] if string.startswith('oa1:' + prefix): @@ -199,10 +215,10 @@ class Plugin(BasePlugin): return (address, name) QMessageBox.warning(self.win, _('Error'), _('No OpenAlias record found.'), _('OK')) return 0 - except dns.resolver.NXDOMAIN: + except resolver.NXDOMAIN: err = _('No such domain.') continue - except dns.resolver.Timeout: + except resolver.Timeout: err = _('Timed out while resolving.') continue except DNSException: @@ -233,7 +249,7 @@ class Plugin(BasePlugin): sub = '.'.join(parts[i - 1:]) query = dns.message.make_query(sub, dns.rdatatype.NS) - response = dns.query.udp(query, ns) + response = dns.query.udp(query, ns, 5) if response.rcode() != dns.rcode.NOERROR: return 0 @@ -251,7 +267,7 @@ class Plugin(BasePlugin): query = dns.message.make_query(sub, dns.rdatatype.DNSKEY, want_dnssec=True) - response = dns.query.udp(query, ns) + response = dns.query.udp(query, ns, 5) if response.rcode() != 0: return 0