URI: 
       tMerge pull request #986 from openalias/master - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 42e586dc463b2c3bf3cc1af7eb5b2d3b22f0c3fc
   DIR parent 9205a35c22e610e0dc9dc919c48fa89bcf6102e4
  HTML 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(-)
       ---
   DIR diff --git a/plugins/openalias.py b/plugins/openalias.py
       t@@ -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