Add ONS Lookup capability

This commit is contained in:
Johnathan Ross 2021-06-11 19:18:27 +10:00
parent 9dbbd57255
commit d1c2d23270
2 changed files with 206 additions and 22 deletions

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,9 @@ 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 config import config
import local_config import local_config
from lmq import FutureJSON, lmq_connection from lmq import FutureJSON, lmq_connection
@ -414,6 +417,109 @@ 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' in onsinfo:
onsinfo = onsinfo['entries'][0]
ons_data[ons_type] = onsinfo
if len(onsinfo['encrypted_value']) in [SESSION_ENCRYPTED_LENGTH, WALLET_ENCRYPTED_LENGTH, LOKINET_ENCRYPTED_LENGTH]:
# 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)
# lokinet check
print(ons_types[ons_type])
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
else:
ons_data[ons_type]['mapping'] = val.hex()
else:
# 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:
# If returned with no data from the RPC
ons_data[ons_type] = True
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>')
@ -687,12 +793,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)
@ -704,13 +810,7 @@ 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',
info=info.get(),
type='bad_search',
id=val,
)
# Initiate all the lookups at once, then redirect to whichever one responds affirmatively # Initiate all the lookups at once, then redirect to whichever one responds affirmatively
snreq = sn_req(lmq, oxend, val) snreq = sn_req(lmq, oxend, val)
blreq = block_header_req(lmq, oxend, val, fail_okay=True) blreq = block_header_req(lmq, oxend, val, fail_okay=True)
@ -719,19 +819,34 @@ def search():
sn = snreq.get() sn = snreq.get()
if sn and 'service_node_states' in sn and sn['service_node_states']: 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) return flask.redirect(flask.url_for('show_sn', pubkey=val), code=301)
bl = blreq.get() bl = blreq.get()
if bl and 'block_header' in bl and bl['block_header']: if bl and 'block_header' in bl and bl['block_header']:
return flask.redirect(flask.url_for('show_block', hash=val), code=301) return flask.redirect(flask.url_for('show_block', hash=val), code=301)
tx = txreq.get() tx = txreq.get()
if tx and 'txs' in tx and tx['txs']: if tx and 'txs' in tx and tx['txs']:
return flask.redirect(flask.url_for('show_tx', txid=val), code=301) return flask.redirect(flask.url_for('show_tx', txid=val), code=301)
# ONS can be of length 64 however with txids, and sn pubkey's being of length 64
# 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)
elif not val or len(val) != 64 or any(c not in string.hexdigits for c in val):
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,
) )
return flask.render_template('not_found.html',
info=info.get(),
type='bad_search',
id=val,
)
@app.route('/api/networkinfo') @app.route('/api/networkinfo')
def api_networkinfo(): def api_networkinfo():
lmq, oxend = lmq_connection() lmq, oxend = lmq_connection()

69
templates/ons.html Normal file
View file

@ -0,0 +1,69 @@
{% 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>
{%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 %}