You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
297 lines
9.7 KiB
297 lines
9.7 KiB
# 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
|
|
import traceback
|
|
|
|
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 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))
|
|
self.previous_payto = ''
|
|
|
|
@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
|
|
if self.win.payto_e.is_pr:
|
|
return
|
|
|
|
url = str(self.win.payto_e.toPlainText())
|
|
|
|
if url == self.previous_payto:
|
|
return
|
|
self.previous_payto = url
|
|
|
|
if not (('.' in url) and (not '<' in url) and (not ' ' in url)):
|
|
return
|
|
|
|
data = self.resolve(url)
|
|
|
|
if not data:
|
|
self.previous_payto = url
|
|
return True
|
|
|
|
address, name = data
|
|
new_url = url + ' <' + address + '>'
|
|
self.win.payto_e.setText(new_url)
|
|
self.previous_payto = new_url
|
|
|
|
if self.config.get('openalias_autoadd') == 'checked':
|
|
self.win.contacts[url] = ('openalias', name)
|
|
self.win.update_contacts_tab()
|
|
|
|
self.win.payto_e.setFrozen(True)
|
|
try:
|
|
self.validated = self.validate_dnssec(url)
|
|
except:
|
|
self.validated = False
|
|
traceback.print_exc(file=sys.stderr)
|
|
|
|
if self.validated:
|
|
self.win.payto_e.setGreen()
|
|
else:
|
|
self.win.payto_e.setExpired()
|
|
|
|
@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
|
|
if self.win.payto_e.is_pr:
|
|
return
|
|
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_())
|
|
|
|
|
|
@hook
|
|
def resolve_address(self, url):
|
|
data = self.resolve(url)
|
|
if not data:
|
|
return
|
|
address, name = data
|
|
try:
|
|
validated = self.validate_dnssec(url)
|
|
except:
|
|
validated = False
|
|
traceback.print_exc(file=sys.stderr)
|
|
return {
|
|
'address': address,
|
|
'name': name,
|
|
'type': 'openalias',
|
|
'validated': validated
|
|
}
|
|
|
|
|
|
def resolve(self, url):
|
|
'''Resolve OpenAlias address using url.'''
|
|
self.print_error('[OA] Attempting to resolve OpenAlias data for ' + url)
|
|
|
|
url = url.replace('@', '.') # support email-style addresses, per the OA standard
|
|
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 False
|
|
|
|
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 False
|
|
# 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 False
|
|
|
|
# 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 False
|
|
|
|
return True
|
|
|