mirror of
https://github.com/oxen-io/oxen-observer.git
synced 2023-12-14 09:22:54 +01:00
Add ONS Lookup capability
This commit is contained in:
parent
9dbbd57255
commit
d1c2d23270
2 changed files with 206 additions and 22 deletions
159
observer.py
159
observer.py
|
@ -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,34 +810,43 @@ def search():
|
||||||
v >>= 4
|
v >>= 4
|
||||||
val = "{:64x}".format(v)
|
val = "{:64x}".format(v)
|
||||||
|
|
||||||
|
if len(val) == 64:
|
||||||
|
# Initiate all the lookups at once, then redirect to whichever one responds affirmatively
|
||||||
|
snreq = sn_req(lmq, oxend, val)
|
||||||
|
blreq = block_header_req(lmq, oxend, val, fail_okay=True)
|
||||||
|
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)
|
||||||
|
|
||||||
|
bl = blreq.get()
|
||||||
|
if bl and 'block_header' in bl and bl['block_header']:
|
||||||
|
return flask.redirect(flask.url_for('show_block', hash=val), code=301)
|
||||||
|
|
||||||
|
tx = txreq.get()
|
||||||
|
if tx and 'txs' in tx and tx['txs']:
|
||||||
|
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):
|
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='bad_search',
|
type='bad_search',
|
||||||
id=val,
|
id=val,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initiate all the lookups at once, then redirect to whichever one responds affirmatively
|
|
||||||
snreq = sn_req(lmq, oxend, val)
|
|
||||||
blreq = block_header_req(lmq, oxend, val, fail_okay=True)
|
|
||||||
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)
|
|
||||||
bl = blreq.get()
|
|
||||||
if bl and 'block_header' in bl and bl['block_header']:
|
|
||||||
return flask.redirect(flask.url_for('show_block', hash=val), code=301)
|
|
||||||
tx = txreq.get()
|
|
||||||
if tx and 'txs' in tx and tx['txs']:
|
|
||||||
return flask.redirect(flask.url_for('show_tx', txid=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()
|
||||||
|
|
69
templates/ons.html
Normal file
69
templates/ons.html
Normal 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 %}
|
Loading…
Reference in a new issue