Merge pull request #6 from Sonofotis/master

Add ONS lookup functionality
This commit is contained in:
Jason Rhinelander 2021-07-15 02:30:47 -03:00 committed by GitHub
commit de31383fd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 404 additions and 22 deletions

168
base58.py Normal file
View File

@ -0,0 +1,168 @@
# MoneroPy - A python toolbox for Monero
# Copyright (C) 2016 The MoneroPy Developers.
#
# MoneroPy is released under the BSD 3-Clause license. Use and redistribution of
# this software is subject to the license terms in the LICENSE file found in the
# top-level directory of this distribution.
__alphabet = [ord(s) for s in '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
__b58base = 58
__UINT64MAX = 2**64
__encodedBlockSizes = [0, 2, 3, 5, 6, 7, 9, 10, 11]
__fullBlockSize = 8
__fullEncodedBlockSize = 11
def _hexToBin(hex):
if len(hex) % 2 != 0:
return "Hex string has invalid length!"
return [int(hex[i*2:i*2+2], 16) for i in range(len(hex)//2)]
def _binToHex(bin):
return "".join([("0" + hex(int(bin[i])).split('x')[1])[-2:] for i in range(len(bin))])
def _strToBin(a):
return [ord(s) for s in a]
def _binToStr(bin):
return ''.join([chr(bin[i]) for i in range(len(bin))])
def _uint8be_to_64(data):
l_data = len(data)
if l_data < 1 or l_data > 8:
return "Invalid input length"
res = 0
switch = 9 - l_data
for i in range(l_data):
if switch == 1:
res = res << 8 | data[i]
elif switch == 2:
res = res << 8 | data[i]
elif switch == 3:
res = res << 8 | data[i]
elif switch == 4:
res = res << 8 | data[i]
elif switch == 5:
res = res << 8 | data[i]
elif switch == 6:
res = res << 8 | data[i]
elif switch == 7:
res = res << 8 | data[i]
elif switch == 8:
res = res << 8 | data[i]
else:
return "Impossible condition"
return res
def _uint64_to_8be(num, size):
res = [0] * size;
if size < 1 or size > 8:
return "Invalid input length"
twopow8 = 2**8
for i in range(size-1,-1,-1):
res[i] = num % twopow8
num = num // twopow8
return res
def encode_block(data, buf, index):
l_data = len(data)
if l_data < 1 or l_data > __fullEncodedBlockSize:
return "Invalid block length: " + str(l_data)
num = _uint8be_to_64(data)
i = __encodedBlockSizes[l_data] - 1
while num > 0:
remainder = num % __b58base
num = num // __b58base
buf[index+i] = __alphabet[remainder];
i -= 1
return buf
def encode(hex):
'''Encode hexadecimal string as base58 (ex: encoding a Monero address).'''
data = _hexToBin(hex)
l_data = len(data)
if l_data == 0:
return ""
full_block_count = l_data // __fullBlockSize
last_block_size = l_data % __fullBlockSize
res_size = full_block_count * __fullEncodedBlockSize + __encodedBlockSizes[last_block_size]
res = [0] * res_size
for i in range(res_size):
res[i] = __alphabet[0]
for i in range(full_block_count):
res = encode_block(data[(i*__fullBlockSize):(i*__fullBlockSize+__fullBlockSize)], res, i * __fullEncodedBlockSize)
if last_block_size > 0:
res = encode_block(data[(full_block_count*__fullBlockSize):(full_block_count*__fullBlockSize+last_block_size)], res, full_block_count * __fullEncodedBlockSize)
return _binToStr(res)
def decode_block(data, buf, index):
l_data = len(data)
if l_data < 1 or l_data > __fullEncodedBlockSize:
return "Invalid block length: " + l_data
res_size = __encodedBlockSizes.index(l_data)
if res_size <= 0:
return "Invalid block size"
res_num = 0
order = 1
for i in range(l_data-1, -1, -1):
digit = __alphabet.index(data[i])
if digit < 0:
return "Invalid symbol"
product = order * digit + res_num
if product > __UINT64MAX:
return "Overflow"
res_num = product
order = order * __b58base
if res_size < __fullBlockSize and 2**(8 * res_size) <= res_num:
return "Overflow 2"
tmp_buf = _uint64_to_8be(res_num, res_size)
for i in range(len(tmp_buf)):
buf[i+index] = tmp_buf[i]
return buf
def decode(enc):
'''Decode a base58 string (ex: a Monero address) into hexidecimal form.'''
enc = _strToBin(enc)
l_enc = len(enc)
if l_enc == 0:
return ""
full_block_count = l_enc // __fullEncodedBlockSize
last_block_size = l_enc % __fullEncodedBlockSize
last_block_decoded_size = __encodedBlockSizes.index(last_block_size)
if last_block_decoded_size < 0:
return "Invalid encoded length"
data_size = full_block_count * __fullBlockSize + last_block_decoded_size
data = [0] * data_size
for i in range(full_block_count):
data = decode_block(enc[(i*__fullEncodedBlockSize):(i*__fullEncodedBlockSize+__fullEncodedBlockSize)], data, i * __fullBlockSize)
if last_block_size > 0:
data = decode_block(enc[(full_block_count*__fullEncodedBlockSize):(full_block_count*__fullEncodedBlockSize+last_block_size)], data, full_block_count * __fullBlockSize)
return _binToHex(data)

View File

@ -9,6 +9,7 @@ import statistics
import string import string
import requests import requests
import time import time
import base64
from base64 import b32encode, b16decode from base64 import b32encode, b16decode
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from pygments import highlight from pygments import highlight
@ -17,7 +18,11 @@ from pygments.formatters import HtmlFormatter
import subprocess import subprocess
import qrcode import qrcode
from io import BytesIO from io import BytesIO
import pysodium
import nacl.encoding
import nacl.hash
import base58
import sha3
import config import config
import local_config import local_config
from lmq import FutureJSON, lmq_connection from lmq import FutureJSON, lmq_connection
@ -418,6 +423,139 @@ def block_with_txs_req(lmq, oxend, hash_or_height, **kwargs):
return FutureJSON(lmq, oxend, 'rpc.get_block', cache_key='single', args=args, **kwargs) return FutureJSON(lmq, oxend, 'rpc.get_block', cache_key='single', args=args, **kwargs)
def ons_info(lmq, oxend, name,ons_type,**kwargs):
if ons_type == 2:
name=name+'.loki'
name_hash = nacl.hash.blake2b(name.encode(), encoder = nacl.encoding.Base64Encoder)
return FutureJSON(lmq, oxend, 'rpc.ons_names_to_owners', args={
"entries": [{'name_hash':name_hash.decode('ascii'),'types':[ons_type]}]})
@app.route('/ons/<string:name>')
@app.route('/ons/<string:name>/<int:more_details>')
def show_ons(name, more_details=False):
name = name.lower()
lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
if len(name) > 64 or not all(c.isalnum() or c in '_-' for c in name):
return flask.render_template('not_found.html',
info=info.get(),
type='bad_search',
id=name,
)
ons_types = {'session':0,'wallet':1,'lokinet':2}
ons_data = {'name':name}
SESSION_ENCRYPTED_LENGTH = 146 # If the encrypted value is not of expected character
WALLET_ENCRYPTED_LENGTH = 210 # length it is of HF15 and before.
LOKINET_ENCRYPTED_LENGTH = 144 # The user must update their session mapping.
for ons_type in ons_types:
onsinfo = ons_info(lmq, oxend, name, ons_types[ons_type]).get()
if 'entries' not in onsinfo:
# If returned with no data from the RPC
if (ons_types[ons_type] == 2 and '-' in name and len(name) > 63) or (ons_types[ons_type] == 2 and '-' not in name and len(name) > 32):
ons_data[ons_type] = False
else:
ons_data[ons_type] = True
else:
onsinfo = onsinfo['entries'][0]
ons_data[ons_type] = onsinfo
if len(onsinfo['encrypted_value']) not in [SESSION_ENCRYPTED_LENGTH, WALLET_ENCRYPTED_LENGTH, LOKINET_ENCRYPTED_LENGTH]:
# Encryption involves a much more expensive argon2-based calculation for HF15 registrations.
# Owners should be notified they should update to the new encryption format.
ons_data[ons_type] = ons_info(lmq, oxend, name,ons_types[ons_type]).get()['entries'][0]
ons_data[ons_type]['mapping'] = 'Owner needs to update their ID for mapping info.'
else:
# RPC returns encrypted_value as ciphertext and nonce concatenated.
# The nonce is the last 48 characters of the encrypted value and the remainder of characters is the encrypted_value.
nonce_received = onsinfo['encrypted_value'][-48:]
nonce = bytes.fromhex(nonce_received)
# The ciphertext is the encrypted_value with the nonce taken away.
ciphertext = bytes.fromhex(onsinfo['encrypted_value'][:-48])
# If ons type is lokinet we need to add .loki to the name before hashing.
if ons_types[ons_type] == 2:
name+='.loki'
# Calculate the blake2b hash of the lower-case full name
name_hash = nacl.hash.blake2b(name.encode(),encoder = nacl.encoding.RawEncoder)
# Decryption key: another blake2b hash, but this time a keyed blake2b hash where the first hash is the key
decryption_key = nacl.hash.blake2b(name.encode(), key=name_hash, encoder = nacl.encoding.RawEncoder)
# XChaCha20+Poly1305 decryption
val = pysodium.crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext=ciphertext, ad=b'', nonce=nonce, key=decryption_key)
if ons_types[ons_type] == 0:
ons_data[ons_type]['mapping'] = val.hex()
continue
if ons_types[ons_type] == 1:
network = val[:1] # For mainnet, primary address. Subaddress is \x74; integrated is \x73; testnet are longer.
if network == b'\x00':
network = b'\x72'
if network == b'\x01':
network = b'\x74'
if len(val) > 65:
network = b'\x73'
val = val[1:]
keccak_hash = sha3.keccak_256()
keccak_hash.update(network)
keccak_hash.update(val)
checksum = keccak_hash.digest()[0:4]
val = network + val + checksum
ons_data[ons_type]['mapping'] = base58.encode(val.hex())
continue
if ons_types[ons_type] == 2:
# val will currently be the raw lokinet ed25519 pubkey (32 bytes). We can convert it to the more
# common lokinet address (which is the same value but encoded in z-base-32) and convert the bytes to
# a string:
val = b32encode(val).decode()
# Python's regular base32 uses a different alphabet, so translate from base32 to z-base-32:
val = val.translate(str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
"ybndrfg8ejkmcpqxot1uwisza345h769"))
# Base32 is also padded with '=', which isn't used in z-base-32:
val = val.rstrip('=')
# Finally slap ".loki" on the end:
val += ".loki"
ons_data[ons_type]['mapping'] = val
continue
if more_details:
formatter = HtmlFormatter(cssclass="syntax-highlight", style="paraiso-dark")
more_details = {
'details_css': formatter.get_style_defs('.syntax-highlight'),
'details_html': highlight(json.dumps(ons_data, indent="\t"), JsonLexer(), formatter),
}
else:
more_details = {}
return flask.render_template('ons.html',
info=info.get(),
ons=ons_data,
**more_details,
)
@app.route('/sn/<hex64:pubkey>') @app.route('/sn/<hex64:pubkey>')
@app.route('/sn/<hex64:pubkey>/<int:more_details>') @app.route('/sn/<hex64:pubkey>/<int:more_details>')
@ -691,12 +829,12 @@ def show_quorums():
base32z_dict = 'ybndrfg8ejkmcpqxot1uwisza345h769' base32z_dict = 'ybndrfg8ejkmcpqxot1uwisza345h769'
base32z_map = {base32z_dict[i]: i for i in range(len(base32z_dict))} base32z_map = {base32z_dict[i]: i for i in range(len(base32z_dict))}
@app.route('/search') @app.route('/search')
def search(): def search():
lmq, oxend = lmq_connection() lmq, oxend = lmq_connection()
info = FutureJSON(lmq, oxend, 'rpc.get_info', 1) info = FutureJSON(lmq, oxend, 'rpc.get_info', 1)
val = (flask.request.args.get('value') or '').strip() val = (flask.request.args.get('value') or '').strip()
if val and len(val) < 10 and val.isdigit(): # Block height if val and len(val) < 10 and val.isdigit(): # Block height
return flask.redirect(flask.url_for('show_block', height=val), code=301) return flask.redirect(flask.url_for('show_block', height=val), code=301)
@ -708,34 +846,39 @@ def search():
v >>= 4 v >>= 4
val = "{:64x}".format(v) val = "{:64x}".format(v)
elif not val or len(val) != 64 or any(c not in string.hexdigits for c in val): if len(val) == 64:
return flask.render_template('not_found.html', # Initiate all the lookups at once, then redirect to whichever one responds affirmatively
info=info.get(), snreq = sn_req(lmq, oxend, val)
type='bad_search', blreq = block_header_req(lmq, oxend, val, fail_okay=True)
id=val, txreq = tx_req(lmq, oxend, [val])
)
sn = snreq.get()
if sn and 'service_node_states' in sn and sn['service_node_states']:
return flask.redirect(flask.url_for('show_sn', pubkey=val), code=301)
# Initiate all the lookups at once, then redirect to whichever one responds affirmatively bl = blreq.get()
snreq = sn_req(lmq, oxend, val) if bl and 'block_header' in bl and bl['block_header']:
blreq = block_header_req(lmq, oxend, val, fail_okay=True) return flask.redirect(flask.url_for('show_block', hash=val), code=301)
txreq = tx_req(lmq, oxend, [val])
sn = snreq.get() tx = txreq.get()
if sn and 'service_node_states' in sn and sn['service_node_states']: if tx and 'txs' in tx and tx['txs']:
return flask.redirect(flask.url_for('show_sn', pubkey=val), code=301) return flask.redirect(flask.url_for('show_tx', txid=val), code=301)
bl = blreq.get()
if bl and 'block_header' in bl and bl['block_header']: if val and len(val) <= 68 and val.endswith(".loki"):
return flask.redirect(flask.url_for('show_block', hash=val), code=301) val = val.rstrip('.loki')
tx = txreq.get()
if tx and 'txs' in tx and tx['txs']: # ONS can be of length 64 however with txids, and sn pubkey's being of length 64
return flask.redirect(flask.url_for('show_tx', txid=val), code=301) # I have removed it from the possible searches.
if len(val) < 64 and all(c.isalnum() or c in '_-' for c in val):
return flask.redirect(flask.url_for('show_ons', name=val), code=301)
return flask.render_template('not_found.html', return flask.render_template('not_found.html',
info=info.get(), info=info.get(),
type='search', type='bad_search',
id=val, id=val,
) )
@app.route('/api/networkinfo') @app.route('/api/networkinfo')
def api_networkinfo(): def api_networkinfo():
lmq, oxend = lmq_connection() lmq, oxend = lmq_connection()

71
templates/ons.html Normal file
View File

@ -0,0 +1,71 @@
{% extends "_basic.html" %}
{% block content %}
<div class="sn-details Wrapper">
<div class="sn-details-main">
<div class="details">
<h2>Oxen Name Server Lookup <b>"{{ons.name}}"</b></h2>
<div class="TitleUnderliner"></div>
<h2><label>Session</label> </h2>
<div class="TitleUnderliner"></div>
{%if ons.session == True%}
<h4 style="margin:5px"><label>Available:</label> Yes</h4>
{%else%}
<h4 style="margin:5px"><label>Available:</label> No</h4>
<h4 style="margin:5px"><label>Owner:</label> {{ons.session.owner}}</h4>
<h4 style="margin:5px"><label>TX Hash:</label> <a href="/tx/{{ons.session.txid}}">{{ons.session.txid}}</a></h4>
<h4 style="margin:5px"><label>Update Height:</label> <a href="/block/{{ons.session.update_height}}">{{ons.session.update_height}}</a></h4>
<h4 style="margin:5px"><label>Name Hash:</label> {{ons.session.name_hash}}</h4>
<h4 style="margin:5px"><label>Mapping:</label> {{ons.session.mapping}}</h4>
{%endif%}
<h2><label>Wallet</label></h2>
<div class="TitleUnderliner"></div>
{%if ons.wallet == True%}
{%if ons.session == True%}
<h4 style="margin:5px"><label>Available:</label> Yes</h4>
{%else%}
<h4 style="margin:5px"><label>Available:</label> Only available for above Session ID Owner</h4>
{%endif%}
{%else%}
<h4 style="margin:5px"><label>Available:</label> No</h4>
<h4 style="margin:5px"><label>Owner:</label> {{ons.wallet.owner}}</h4>
<h4 style="margin:5px"><label>TX Hash:</label> <a href="/tx/{{ons.wallet.txid}}">{{ons.wallet.txid}}</a></h4>
<h4 style="margin:5px"><label>Update Height:</label> <a href="/block/{{ons.wallet.update_height}}">{{ons.wallet.update_height}}</a></h4>
<h4 style="margin:5px"><label>Name Hash:</label> {{ons.wallet.name_hash}}</h4>
<h4 style="margin:5px"><label>Mapping:</label> {{ons.wallet.mapping}}</h4>
{%endif%}
<h2><label>Lokinet</label></h2>
<div class="TitleUnderliner"></div>
{%if ons.lokinet == True%}
<h4 style="margin:5px"><label>Available:</label> Yes</h4>
{%elif ons.lokinet == False%}
<h4 style="margin:5px"><label>Available:</label> No</h4>
{%else%}
<h4 style="margin:5px"><label>Available:</label> No</h4>
<h4 style="margin:5px"><label>Owner:</label> {{ons.lokinet.owner}}</h4>
<h4 style="margin:5px"><label>TX Hash:</label> <a href="/tx/{{ons.session.txid}}">{{ons.lokinet.txid}}</a></h4>
<h4 style="margin:5px"><label>Update Height:</label> <a href="/block/{{ons.lokinet.update_height}}">{{ons.lokinet.update_height}}</a></h4>
<h4 style="margin:5px"><label>Expiration Height:</label> {{ons.lokinet.expiration_height}}</h4>
<h4 style="margin:5px"><label>Name Hash:</label> {{ons.lokinet.name_hash}}</h4>
<h4 style="margin:5px"><label>Mapping:</label> {{ons.lokinet.mapping}}</h4>
{%endif%}
</div>
</div>
{%if details_html%}
<style type="text/css">
{{details_css | safe}}
</style>
<div class="TitleDivider" id="more_details"></div>
{{details_html | safe}}
{%else%}
<h5>
<a href="/ons/{{ons.name}}/1#more_details">Show raw details</a>
</h5>
{%endif%}
</div>
{% endblock %}