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',