From b15a890eef1d3d72e952fdc146ec57c89010fdbf Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Wed, 14 Jan 2015 23:07:48 +0200 Subject: [PATCH 01/17] 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 00000000..3a80da62 --- /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 f3beb76f..e728f5c5 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 02/17] fixed incorrect dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e728f5c5..1d6eca54 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 03/17] 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 3a80da62..bdaae463 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 04/17] 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 bdaae463..0143f6b8 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 05/17] 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 0143f6b8..545f789b 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 From a7ce38945156dde8419804a08b6e33a54bcb6b1e Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Wed, 14 Jan 2015 23:07:48 +0200 Subject: [PATCH 06/17] 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 00000000..3a80da62 --- /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 f3beb76f..e728f5c5 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 038c57994f118cf6157349a55e240972def91cc0 Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Thu, 15 Jan 2015 21:07:08 +0200 Subject: [PATCH 07/17] fixed incorrect dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e728f5c5..1d6eca54 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( 'qrcode', 'SocksiPy-branch', 'tlslite', - 'pythondns' + 'dnspython' ], package_dir={ 'electrum': 'lib', From f71dd322ddefdd75a7d768a44f383e0519c0761d Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Sun, 18 Jan 2015 13:48:28 +0200 Subject: [PATCH 08/17] 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 3a80da62..bdaae463 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 a0739ad9259d894b404572afc425c5dd53dbd8eb Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Sun, 18 Jan 2015 14:18:28 +0200 Subject: [PATCH 09/17] 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 bdaae463..0143f6b8 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 1e73768b899db5e738c42fc2f369d84d8e55b17a Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Mon, 19 Jan 2015 16:49:46 +0200 Subject: [PATCH 10/17] 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 0143f6b8..545f789b 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 From 714db0f5a1e1278e4a69f2dff42a8c9ea6f079f6 Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Fri, 23 Jan 2015 20:11:34 +0200 Subject: [PATCH 11/17] tweaked timeouts, handle DNSSEC check errors --- plugins/openalias.py | 120 +++++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index 545f789b..07485b01 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -1,5 +1,6 @@ from electrum_gui.qt.util import EnterButton from electrum.plugins import BasePlugin, hook +from electrum.util import print_msg from electrum.i18n import _ from PyQt4.QtGui import * from PyQt4.QtCore import * @@ -43,6 +44,7 @@ class Plugin(BasePlugin): return OA_READY def __init__(self, gui, name): + print_msg('[OA] Initialiasing OpenAlias plugin, OA_READY is ' + str(OA_READY)) BasePlugin.__init__(self, gui, name) self._is_available = OA_READY @@ -82,9 +84,12 @@ class Plugin(BasePlugin): if not data: return True + (address, name) = data + self.win.payto_e.setText(address) + if not self.validate_dnssec(url): msgBox = QMessageBox() - msgBox.setText(_('No valid DNSSEC trust chain!')) + msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.')) msgBox.setInformativeText(_('Do you wish to continue?')) msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) msgBox.setDefaultButton(QMessageBox.Cancel) @@ -92,8 +97,6 @@ class Plugin(BasePlugin): 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 @@ -150,9 +153,11 @@ class Plugin(BasePlugin): if not data: return + (address, name) = data + if not self.validate_dnssec(url): msgBox = QMessageBox() - msgBox.setText("No valid DNSSEC trust chain!") + msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.')) msgBox.setInformativeText("Do you wish to continue?") msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) msgBox.setDefaultButton(QMessageBox.Cancel) @@ -160,8 +165,6 @@ class Plugin(BasePlugin): if reply != QMessageBox.Ok: return - (address, name) = data - d2 = QDialog(self.win) vbox2 = QVBoxLayout(d2) grid2 = QGridLayout() @@ -196,15 +199,17 @@ class Plugin(BasePlugin): def resolve(self, url): '''Resolve OpenAlias address using url.''' + print_msg('[OA] Attempting to resolve OpenAlias data for ' + url) + prefix = 'btc' retries = 3 err = None for i in range(0, retries): try: resolver = dns.resolver.Resolver() - resolver.timeout = 15.0 - resolver.lifetime = 15.0 - records = resolver.query(url, 'TXT') + resolver.timeout = 2.0 + resolver.lifetime = 2.0 + records = resolver.query(url, dns.rdatatype.TXT) for record in records: string = record.strings[0] if string.startswith('oa1:' + prefix): @@ -215,10 +220,10 @@ class Plugin(BasePlugin): return (address, name) QMessageBox.warning(self.win, _('Error'), _('No OpenAlias record found.'), _('OK')) return 0 - except resolver.NXDOMAIN: + except dns.resolver.NXDOMAIN: err = _('No such domain.') continue - except resolver.Timeout: + except dns.resolver.Timeout: err = _('Timed out while resolving.') continue except DNSException: @@ -240,48 +245,53 @@ class Plugin(BasePlugin): 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, 5) - - 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, 5) - - 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 + print_msg('[OA] Checking DNSSEC trust chain for ' + url) + + try: + 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, 1) + + 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, 1) + + 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 + except Exception,e: + return 0 return 1 \ No newline at end of file From 78f90a0f264379adf4f7a223028ce68223e64dde Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Fri, 23 Jan 2015 20:21:43 +0200 Subject: [PATCH 12/17] handle @ characters in the address --- plugins/openalias.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/openalias.py b/plugins/openalias.py index 07485b01..5093dcfb 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -72,6 +72,8 @@ class Plugin(BasePlugin): return False url = str(self.win.payto_e.toPlainText()) + url = url.replace('@', '.') + if not '.' in url: return False else: @@ -143,6 +145,8 @@ class Plugin(BasePlugin): return url = str(line1.text()) + + url = url.replace('@', '.') if not '.' in url: QMessageBox.warning(self.win, _('Error'), _('Invalid URL'), _('OK')) From e96fe36e62d7e323d175ef05b4b1bfab13d7b1e9 Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Fri, 23 Jan 2015 20:50:34 +0200 Subject: [PATCH 13/17] add license and info, handle address with no name --- plugins/openalias.py | 45 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index 5093dcfb..7a8e1a8f 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -1,3 +1,40 @@ +# Copyright (c) 2014-2015, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This plugin implements the OpenAlias standard. For information on the standard please +# see: https://openalias.org + +# Donations for ongoing development of the standard and hosting resolvers can be sent to +# openalias.org or donate.monero.cc + +# Version: 0.1 +# Todo: optionally use OA resolvers; add DNSCrypt support + from electrum_gui.qt.util import EnterButton from electrum.plugins import BasePlugin, hook from electrum.util import print_msg @@ -7,6 +44,8 @@ from PyQt4.QtCore import * import re +# Import all of the rdtypes, as py2app and similar get confused with the dnspython +# autoloader and won't include all the rdatatypes try: import dns.name import dns.query @@ -72,12 +111,12 @@ class Plugin(BasePlugin): return False url = str(self.win.payto_e.toPlainText()) - url = url.replace('@', '.') + url = url.replace('@', '.') # support email-style addresses, per the OA standard if not '.' in url: return False else: - if not OA_READY: + if not OA_READY: # handle a failed DNSPython load 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 @@ -219,6 +258,8 @@ class Plugin(BasePlugin): 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 name: + name = address if not address: continue return (address, name) From 540adeb22c503dcc69ec2afb53707f98d0bbeb57 Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Sat, 24 Jan 2015 19:50:28 +0200 Subject: [PATCH 14/17] use the correct Electrum alias syntax --- plugins/openalias.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index 7a8e1a8f..5635f35e 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -113,12 +113,12 @@ class Plugin(BasePlugin): url = url.replace('@', '.') # support email-style addresses, per the OA standard - if not '.' in url: - return False - else: + if ('.' in url) and (not '<' in url) and (not ' ' in url): if not OA_READY: # handle a failed DNSPython load 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 + else: + return False data = self.resolve(url) @@ -126,7 +126,7 @@ class Plugin(BasePlugin): return True (address, name) = data - self.win.payto_e.setText(address) + self.win.payto_e.setText(url + ' <' + address + '>') if not self.validate_dnssec(url): msgBox = QMessageBox() From 090816998ec5c55ffe9c6d5991f675f444047dfb Mon Sep 17 00:00:00 2001 From: Bartosz Dabkowski Date: Wed, 4 Feb 2015 14:44:50 +0100 Subject: [PATCH 15/17] Resolve address when lost focus. --- plugins/openalias.py | 100 ++++++++++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index 5635f35e..feb58c24 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -1,21 +1,21 @@ # Copyright (c) 2014-2015, The Monero Project -# +# # All rights reserved. -# +# # Redistribution and use in source and binary forms, with or without modification, are # permitted provided that the following conditions are met: -# +# # 1. Redistributions of source code must retain the above copyright notice, this list of # conditions and the following disclaimer. -# +# # 2. Redistributions in binary form must reproduce the above copyright notice, this list # of conditions and the following disclaimer in the documentation and/or other # materials provided with the distribution. -# +# # 3. Neither the name of the copyright holder nor the names of its contributors may be # used to endorse or promote products derived from this software without specific # prior written permission. -# +# # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL @@ -98,6 +98,36 @@ class Plugin(BasePlugin): def settings_widget(self, window): return EnterButton(_('Settings'), self.settings_dialog) + @hook + def timer_actions(self): + if self.win.payto_e.hasFocus(): + return + if self.win.payto_e.is_multiline(): # only supports single line entries atm + return + url = str(self.win.payto_e.toPlainText()) + if url == self.win.previous_payto_e: + return + self.win.previous_payto_e = url + url = url.replace('@', '.') # support email-style addresses, per the OA standard + + if ('.' in url) and (not '<' in url) and (not ' ' in url): + if not OA_READY: # handle a failed DNSPython load + QMessageBox.warning(self.win, _('Error'), 'Could not load DNSPython libraries, please ensure they are available and/or Electrum has been built correctly', _('OK')) + return + else: + return + + data = self.resolve(url) + + if not data: + self.win.previous_payto_e = url + return True + + (address, name) = data + new_url = url + ' <' + address + '>' + self.win.payto_e.setText(new_url) + self.win.previous_payto_e = new_url + @hook def before_send(self): ''' @@ -109,28 +139,20 @@ class Plugin(BasePlugin): if self.win.payto_e.is_multiline(): # only supports single line entries atm return False - url = str(self.win.payto_e.toPlainText()) - - url = url.replace('@', '.') # support email-style addresses, per the OA standard - - if ('.' in url) and (not '<' in url) and (not ' ' in url): - if not OA_READY: # handle a failed DNSPython load - 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 - else: + payto_e = str(self.win.payto_e.toPlainText()) + regex = re.compile(r'^([^\s]+) <([A-Za-z0-9]+)>') # only do that for converted addresses + try: + (url, address) = regex.search(payto_e).groups() + except AttributeError: return False - data = self.resolve(url) - - if not data: + if not OA_READY: # handle a failed DNSPython load + QMessageBox.warning(self.win, _('Error'), 'Could not load DNSPython libraries, please ensure they are available and/or Electrum has been built correctly', _('OK')) return True - (address, name) = data - self.win.payto_e.setText(url + ' <' + address + '>') - if not self.validate_dnssec(url): msgBox = QMessageBox() - msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.')) + msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.')) msgBox.setInformativeText(_('Do you wish to continue?')) msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) msgBox.setDefaultButton(QMessageBox.Cancel) @@ -184,7 +206,7 @@ class Plugin(BasePlugin): return url = str(line1.text()) - + url = url.replace('@', '.') if not '.' in url: @@ -200,7 +222,7 @@ class Plugin(BasePlugin): if not self.validate_dnssec(url): msgBox = QMessageBox() - msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.')) + msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.')) msgBox.setInformativeText("Do you wish to continue?") msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok) msgBox.setDefaultButton(QMessageBox.Cancel) @@ -243,7 +265,7 @@ class Plugin(BasePlugin): def resolve(self, url): '''Resolve OpenAlias address using url.''' print_msg('[OA] Attempting to resolve OpenAlias data for ' + url) - + prefix = 'btc' retries = 3 err = None @@ -274,7 +296,7 @@ class Plugin(BasePlugin): except DNSException: err = _('Unhandled exception.') continue - except Exception,e: + except Exception, e: err = _('Unexpected error: ' + str(e)) continue break @@ -291,52 +313,52 @@ class Plugin(BasePlugin): def validate_dnssec(self, url): print_msg('[OA] Checking DNSSEC trust chain for ' + url) - + try: 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, 1) - + 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, 1) - + 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 - except Exception,e: + except Exception, e: return 0 - return 1 \ No newline at end of file + return 1 From 67b39e67d4b1d115f38f07a08dde01485d3d578b Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Wed, 11 Feb 2015 21:40:58 +0200 Subject: [PATCH 16/17] fixed incorrect code order, handle @ chars correctly --- plugins/openalias.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/openalias.py b/plugins/openalias.py index feb58c24..669b0544 100644 --- a/plugins/openalias.py +++ b/plugins/openalias.py @@ -104,11 +104,13 @@ class Plugin(BasePlugin): return if self.win.payto_e.is_multiline(): # only supports single line entries atm return + url = str(self.win.payto_e.toPlainText()) + url = url.replace('@', '.') # support email-style addresses, per the OA standard + if url == self.win.previous_payto_e: return self.win.previous_payto_e = url - url = url.replace('@', '.') # support email-style addresses, per the OA standard if ('.' in url) and (not '<' in url) and (not ' ' in url): if not OA_READY: # handle a failed DNSPython load From 41571de1e6f4aa03f914a99500a08a7de07e1c38 Mon Sep 17 00:00:00 2001 From: Riccardo Spagni Date: Tue, 17 Feb 2015 15:25:05 +0200 Subject: [PATCH 17/17] revert dnspython dependency in setup.py --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 264743e0..6486aea3 100644 --- a/setup.py +++ b/setup.py @@ -74,8 +74,7 @@ setup( 'qrcode', 'SocksiPy-branch', 'protobuf', - 'tlslite', - 'dnspython' + 'tlslite' ], package_dir={ 'electrum': 'lib',