electrum-bitcoinprivate/plugins/openalias.py

274 lines
9.2 KiB
Python

# Copyright (c) 2014-2015, The Monero Project
#
# All rights reserved.
# This plugin is licensed under the GPL v3 license (see the LICENSE file in the base of
# the project source code). The Monero Project reserves the right to change this license
# in future to match or be compliant with any relicense of the Electrum project.
# 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
import re
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from electrum_gui.qt.util import *
from electrum.plugins import BasePlugin, hook
from electrum.util import print_error
from electrum.i18n import _
# 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.\nRequires dnspython'
def is_available(self):
return OA_READY
def __init__(self, gui, name):
BasePlugin.__init__(self, gui, name)
self._is_available = OA_READY
self.print_error('OA_READY is ' + str(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 not (('.' in url) and (not '<' in url) and (not ' ' in url)):
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
if self.config.get('openalias_autoadd') == 'checked':
self.win.wallet.add_contact(url, name)
self.win.update_contacts_tab()
self.win.payto_e.setFrozen(True)
if self.validate_dnssec(url):
self.win.payto_e.setGreen()
self.validated = True
else:
self.win.payto_e.setExpired()
self.validated = False
@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 self.validated:
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
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 resolve(self, url):
'''Resolve OpenAlias address using url.'''
self.print_error('[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 = 4.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):
self.print_error('Checking DNSSEC trust chain for ' + 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, 3)
if response.rcode() != dns.rcode.NOERROR:
self.print_error("query error")
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, 3)
if response.rcode() != 0:
self.print_error("query error")
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:
self.print_error("answer error", answer)
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:
self.print_error("validation error")
return 0
return 1