move openalias from plugins to core

This commit is contained in:
ThomasV 2015-07-02 12:44:53 +02:00
parent 90d32038fa
commit 616becd9a8
7 changed files with 238 additions and 339 deletions

View File

@ -19,6 +19,10 @@
import sys, time, threading
import os.path, json, traceback
import shutil
import socket
import webbrowser
import csv
from decimal import Decimal
import PyQt4
@ -26,11 +30,10 @@ from PyQt4.QtGui import *
from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore
from electrum.bitcoin import MIN_RELAY_TX_FEE, COIN, is_valid
from electrum.plugins import run_hook
import icons_rc
from electrum.bitcoin import MIN_RELAY_TX_FEE, COIN, is_valid
from electrum.plugins import run_hook
from electrum.i18n import _
from electrum.util import block_explorer, block_explorer_info, block_explorer_URL
from electrum.util import print_error, print_msg
@ -41,6 +44,7 @@ from electrum import util, bitcoin, commands, Wallet
from electrum import SimpleConfig, Wallet, WalletStorage
from electrum import Imported_Wallet
from electrum import paymentrequest
from electrum.contacts import Contacts
from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit
from network_dialog import NetworkDialog
@ -48,12 +52,6 @@ from qrcodewidget import QRCodeWidget, QRDialog
from qrtextedit import ScanQRTextEdit, ShowQRTextEdit
from transaction_dialog import show_transaction
from decimal import Decimal
import socket
import webbrowser
import csv
@ -120,7 +118,7 @@ class ElectrumWindow(QMainWindow):
self.app = gui_object.app
self.invoices = InvoiceStore(self.config)
self.contacts = util.Contacts(self.config)
self.contacts = Contacts(self.config)
self.create_status_bar()
self.need_update = threading.Event()
@ -482,17 +480,16 @@ class ElectrumWindow(QMainWindow):
if self.need_update.is_set():
self.update_wallet()
self.need_update.clear()
# resolve aliases
self.payto_e.resolve()
run_hook('timer_actions')
def format_amount(self, x, is_diff=False, whitespaces=False):
return format_satoshis(x, is_diff, self.num_zeros, self.decimal_point, whitespaces)
def get_decimal_point(self):
return self.decimal_point
def base_unit(self):
assert self.decimal_point in [2, 5, 8]
if self.decimal_point == 2:
@ -1051,6 +1048,13 @@ class ElectrumWindow(QMainWindow):
return
outputs = self.payto_e.get_outputs()
if self.payto_e.is_alias and self.payto_e.validated is False:
alias = self.payto_e.toPlainText()
msg = _('WARNING: the alias "%s" could not be validated via an additional security check, DNSSEC, and thus may not be correct.'%alias) + '\n'
msg += _('Do you wish to continue?')
if not self.question(msg):
return
if not outputs:
QMessageBox.warning(self, _('Error'), _('No outputs'), _('OK'))
return
@ -1208,6 +1212,7 @@ class ElectrumWindow(QMainWindow):
self.payment_request = None
return
self.payto_e.is_pr = True
if not pr.has_expired():
self.payto_e.setGreen()
else:

View File

@ -34,6 +34,7 @@ class PayToEdit(ScanQRTextEdit):
def __init__(self, win):
ScanQRTextEdit.__init__(self)
self.win = win
self.amount_edit = win.amount_e
self.document().contentsChanged.connect(self.update_size)
self.heightMin = 0
@ -43,10 +44,13 @@ class PayToEdit(ScanQRTextEdit):
self.outputs = []
self.errors = []
self.is_pr = False
self.is_alias = False
self.scan_f = win.pay_from_URI
self.update_size()
self.payto_address = None
self.previous_payto = ''
def lock_amount(self):
self.amount_edit.setFrozen(True)
@ -60,11 +64,9 @@ class PayToEdit(ScanQRTextEdit):
button.setHidden(b)
def setGreen(self):
self.is_pr = True
self.setStyleSheet("QWidget { background-color:#80ff80;}")
def setExpired(self):
self.is_pr = True
self.setStyleSheet("QWidget { background-color:#ffcccc;}")
def parse_address_and_amount(self, line):
@ -252,3 +254,45 @@ class PayToEdit(ScanQRTextEdit):
if data.startswith("bitcoin:"):
self.scan_f(data)
# TODO: update fee
def resolve(self):
self.is_alias = False
if self.hasFocus():
return
if self.is_multiline(): # only supports single line entries atm
return
if self.is_pr:
return
key = str(self.toPlainText())
if key == self.previous_payto:
return
self.previous_payto = key
if not (('.' in key) and (not '<' in key) and (not ' ' in key)):
return
try:
data = self.win.contacts.resolve(key)
except:
return
if not data:
return
self.is_alias = True
address = data.get('address')
name = data.get('name')
new_url = key + ' <' + address + '>'
self.setText(new_url)
self.previous_payto = new_url
#if self.win.config.get('openalias_autoadd') == 'checked':
self.win.contacts[key] = ('openalias', name)
self.win.update_contacts_tab()
self.setFrozen(True)
if data.get('type') == 'openalias':
self.validated = data.get('validated')
if self.validated:
self.setGreen()
else:
self.setExpired()
else:
self.validated = None

View File

@ -34,6 +34,7 @@ from bitcoin import is_address, hash_160_to_bc_address, hash_160, COIN
from transaction import Transaction
import paymentrequest
from paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
import contacts
known_commands = {}
@ -78,7 +79,7 @@ class Commands:
self.network = network
self._callback = callback
self.password = None
self.contacts = util.Contacts(self.config)
self.contacts = contacts.Contacts(self.config)
def _run(self, method, args, password_getter):
cmd = known_commands[method]

172
lib/contacts.py Normal file
View File

@ -0,0 +1,172 @@
import sys
import re
import dns
import traceback
import bitcoin
from util import StoreDict, print_error
from 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 Contacts(StoreDict):
def __init__(self, config):
StoreDict.__init__(self, config, 'contacts')
def resolve(self, k):
if bitcoin.is_address(k):
return {
'address': k,
'type': 'address'
}
if k in self.keys():
_type, addr = self[k]
if _type == 'address':
return {
'address': addr,
'type': 'contact'
}
out = self.resolve_openalias(k)
if out:
address, name = out
try:
validated = self.validate_dnssec(k)
except:
validated = False
traceback.print_exc(file=sys.stderr)
return {
'address': address,
'name': name,
'type': 'openalias',
'validated': validated
}
raise Exception("Invalid Bitcoin address or alias", k)
def resolve_openalias(self, url):
'''Resolve OpenAlias address using url.'''
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)
err = _('No OpenAlias record found.')
break
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:
print_error(err)
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_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:
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:
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:
print_error("validation error")
return False
return True

View File

@ -469,22 +469,3 @@ class StoreDict(dict):
self.save()
import bitcoin
from plugins import run_hook
class Contacts(StoreDict):
def __init__(self, config):
StoreDict.__init__(self, config, 'contacts')
def resolve(self, k):
if bitcoin.is_address(k):
return {'address':k, 'type':'address'}
if k in self.keys():
_type, addr = self[k]
if _type == 'address':
return {'address':addr, 'type':'contact'}
out = run_hook('resolve_address', k)
if out:
return out
raise Exception("Invalid Bitcoin address or alias", k)

View File

@ -68,13 +68,6 @@ descriptions = [
]),
'available_for': ['qt']
},
{
'name': 'openalias',
'fullname': 'OpenAlias',
'description': _('Allow for payments to OpenAlias addresses.'),
'requires': [('dns', 'dnspython')],
'available_for': ['qt', 'cmdline']
},
{
'name': 'plot',
'fullname': 'Plot History',

View File

@ -1,297 +0,0 @@
# 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