Files
purple-electrumwallet/plugins/openalias.py
T

274 lines
9.2 KiB
Python
Raw Normal View History

# 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
2015-03-14 12:28:19 +01:00
import re
from PyQt4.QtGui import *
from PyQt4.QtCore import *
from electrum_gui.qt.util import *
2015-01-14 23:07:48 +02:00
from electrum.plugins import BasePlugin, hook
from electrum.util import print_error
2015-01-14 23:07:48 +02:00
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
2015-01-18 14:18:28 +02:00
try:
import dns.name
import dns.query
import dns.dnssec
import dns.message
import dns.resolver
import dns.rdatatype
2015-01-19 16:49:46 +02:00
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
2015-01-18 14:18:28 +02:00
from dns.exception import DNSException
OA_READY = True
except ImportError:
OA_READY = False
2015-01-14 23:07:48 +02:00
class Plugin(BasePlugin):
def fullname(self):
return 'OpenAlias'
def description(self):
return 'Allow for payments to OpenAlias addresses.\nRequires dnspython'
2015-01-18 14:18:28 +02:00
def is_available(self):
return OA_READY
2015-01-14 23:07:48 +02:00
def __init__(self, gui, name):
BasePlugin.__init__(self, gui, name)
2015-01-18 14:18:28 +02:00
self._is_available = OA_READY
self.print_error('OA_READY is ' + str(OA_READY))
2015-01-14 23:07:48 +02:00
@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
2015-02-04 14:44:50 +01:00
def timer_actions(self):
if self.win.payto_e.hasFocus():
return
2015-01-14 23:07:48 +02:00
if self.win.payto_e.is_multiline(): # only supports single line entries atm
2015-02-04 14:44:50 +01:00
return
2015-01-14 23:07:48 +02:00
url = str(self.win.payto_e.toPlainText())
url = url.replace('@', '.') # support email-style addresses, per the OA standard
2015-02-04 14:44:50 +01:00
if url == self.win.previous_payto_e:
return
self.win.previous_payto_e = url
2015-01-23 20:21:43 +02:00
2015-03-31 09:40:53 +02:00
if not (('.' in url) and (not '<' in url) and (not ' ' in url)):
2015-02-04 14:44:50 +01:00
return
2015-01-14 23:07:48 +02:00
data = self.resolve(url)
if not data:
2015-02-04 14:44:50 +01:00
self.win.previous_payto_e = url
2015-01-14 23:07:48 +02:00
return True
2015-03-31 09:35:11 +02:00
address, name = data
2015-02-04 14:44:50 +01:00
new_url = url + ' <' + address + '>'
self.win.payto_e.setText(new_url)
self.win.previous_payto_e = new_url
2015-03-31 09:35:11 +02:00
if self.config.get('openalias_autoadd') == 'checked':
2015-03-31 12:02:57 +02:00
self.win.wallet.add_contact(url, name)
2015-03-31 13:41:46 +02:00
self.win.update_contacts_tab()
2015-03-31 09:35:11 +02:00
2015-03-31 09:37:09 +02:00
self.win.payto_e.setFrozen(True)
if self.validate_dnssec(url):
self.win.payto_e.setGreen()
self.validated = True
2015-03-31 09:37:09 +02:00
else:
self.win.payto_e.setExpired()
self.validated = False
2015-03-31 09:37:09 +02:00
2015-02-04 14:44:50 +01:00
@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:
2015-01-14 23:07:48 +02:00
msgBox = QMessageBox()
2015-02-04 14:44:50 +01:00
msgBox.setText(_('WARNING: the address ' + address + ' could not be validated via an additional security check, DNSSEC, and thus may not be correct.'))
2015-01-14 23:07:48 +02:00
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)
2015-02-04 14:44:50 +01:00
2015-01-14 23:07:48 +02:00
prefix = 'btc'
retries = 3
err = None
for i in range(0, retries):
try:
2015-01-19 16:49:46 +02:00
resolver = dns.resolver.Resolver()
resolver.timeout = 2.0
resolver.lifetime = 4.0
records = resolver.query(url, dns.rdatatype.TXT)
2015-01-14 23:07:48 +02:00
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
2015-01-14 23:07:48 +02:00
if not address:
continue
return (address, name)
QMessageBox.warning(self.win, _('Error'), _('No OpenAlias record found.'), _('OK'))
return 0
except dns.resolver.NXDOMAIN:
2015-01-14 23:07:48 +02:00
err = _('No such domain.')
continue
except dns.resolver.Timeout:
2015-01-14 23:07:48 +02:00
err = _('Timed out while resolving.')
continue
except DNSException:
err = _('Unhandled exception.')
continue
2015-02-04 14:44:50 +01:00
except Exception, e:
2015-01-18 13:48:28 +02:00
err = _('Unexpected error: ' + str(e))
2015-01-14 23:07:48 +02:00
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:
2015-03-31 12:21:20 +02:00
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
2015-02-04 14:44:50 +01:00
return 1