commit 42e586dc463b2c3bf3cc1af7eb5b2d3b22f0c3fc
parent 9205a35c22e610e0dc9dc919c48fa89bcf6102e4
Author: ThomasV <electrumdev@gmail.com>
Date: Tue, 17 Feb 2015 15:03:17 +0100
Merge pull request #986 from openalias/master
OpenAlias: Plugin v0.1
Diffstat:
A | plugins/openalias.py | | | 366 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 366 insertions(+), 0 deletions(-)
diff --git a/plugins/openalias.py b/plugins/openalias.py
@@ -0,0 +1,366 @@
+# 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
+from electrum.i18n import _
+from PyQt4.QtGui import *
+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
+ import dns.dnssec
+ 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:
+ OA_READY = False
+
+
+class Plugin(BasePlugin):
+ def fullname(self):
+ return 'OpenAlias'
+
+ def description(self):
+ return 'Allow for payments to OpenAlias addresses.'
+
+ def is_available(self):
+ 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
+
+ @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 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())
+ 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
+
+ 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):
+ '''
+ 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
+ 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
+
+ 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
+
+ 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.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
+
+ 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())
+
+ url = url.replace('@', '.')
+
+ if not '.' in url:
+ QMessageBox.warning(self.win, _('Error'), _('Invalid URL'), _('OK'))
+ return
+
+ data = self.resolve(url)
+
+ if not data:
+ return
+
+ (address, name) = data
+
+ 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.setInformativeText("Do you wish to continue?")
+ msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
+ msgBox.setDefaultButton(QMessageBox.Cancel)
+ reply = msgBox.exec_()
+ if reply != QMessageBox.Ok:
+ return
+
+ 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.'''
+ 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 = 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):
+ 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)
+ 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 Exception, e:
+ err = _('Unexpected error: ' + str(e))
+ 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):
+ 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