feat: Add TXT login using HIP 13
All checks were successful
Build Docker / Build Docker (push) Successful in 27s
All checks were successful
Build Docker / Build Docker (push) Successful in 27s
This commit is contained in:
parent
f6009fc335
commit
e66ae58494
@ -1,6 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
from .varo_auth import flask_login as varo_auth_flask_login
|
from .varo_auth import flask_login as varo_auth_flask_login
|
||||||
from flask import Blueprint, request, session, url_for
|
from flask import Blueprint, request, session, url_for, make_response
|
||||||
from flask import render_template, redirect, jsonify, send_from_directory
|
from flask import render_template, redirect, jsonify, send_from_directory
|
||||||
from werkzeug.security import gen_salt
|
from werkzeug.security import gen_salt
|
||||||
from authlib.integrations.flask_oauth2 import current_token
|
from authlib.integrations.flask_oauth2 import current_token
|
||||||
@ -40,6 +40,14 @@ def split_by_crlf(s):
|
|||||||
@bp.route("/", methods=("GET", "POST"))
|
@bp.route("/", methods=("GET", "POST"))
|
||||||
def home():
|
def home():
|
||||||
next_page = request.args.get("next")
|
next_page = request.args.get("next")
|
||||||
|
|
||||||
|
# Check if session exists
|
||||||
|
if "uuid" not in session:
|
||||||
|
session["uuid"] = gen_salt(24)
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
|
uuid = session["uuid"]
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
auth = varo_auth_flask_login(request)
|
auth = varo_auth_flask_login(request)
|
||||||
if auth == False:
|
if auth == False:
|
||||||
@ -83,9 +91,15 @@ def home():
|
|||||||
if openseaInfo.status_code == 200:
|
if openseaInfo.status_code == 200:
|
||||||
hnsid = openseaInfo.json()
|
hnsid = openseaInfo.json()
|
||||||
|
|
||||||
|
domains = []
|
||||||
|
if 'domains' in session:
|
||||||
|
domains = session['domains']
|
||||||
|
|
||||||
|
|
||||||
return render_template("home.html", user=user, clients=clients, address=address, hnsid=hnsid, users=users)
|
|
||||||
|
return render_template("home.html", user=user, clients=clients,
|
||||||
|
address=address, hnsid=hnsid, users=users,
|
||||||
|
uuid=uuid, next=next_page, domains=domains)
|
||||||
|
|
||||||
@bp.route("/hnsid", methods=["POST"])
|
@bp.route("/hnsid", methods=["POST"])
|
||||||
def hnsid():
|
def hnsid():
|
||||||
@ -142,6 +156,129 @@ def hnsid_domain(domain):
|
|||||||
|
|
||||||
return jsonify({"success": False, "error": "Domain not found"})
|
return jsonify({"success": False, "error": "Domain not found"})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/txt", methods=["POST"])
|
||||||
|
def txtLogin():
|
||||||
|
# Get domain from form
|
||||||
|
domain = request.form.get("domain")
|
||||||
|
# Get uuid
|
||||||
|
uuid = session["uuid"]
|
||||||
|
|
||||||
|
query = dns.message.make_query(domain, dns.rdatatype.TXT)
|
||||||
|
dns_request = query.to_wire()
|
||||||
|
|
||||||
|
# Send the DNS query over HTTPS
|
||||||
|
response = requests.post('https://hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
|
||||||
|
|
||||||
|
# Parse the DNS response
|
||||||
|
dns_response = dns.message.from_wire(response.content)
|
||||||
|
|
||||||
|
# Loop over TXT records and look for profile avatar
|
||||||
|
idns_records = []
|
||||||
|
for record in dns_response.answer:
|
||||||
|
if record.rdtype == dns.rdatatype.TXT:
|
||||||
|
for txt in record:
|
||||||
|
txt_value:str = txt.to_text().strip('"')
|
||||||
|
if txt_value.startswith("IDNS1"):
|
||||||
|
print(txt_value)
|
||||||
|
idns = txt_value.removeprefix("IDNS1 ")
|
||||||
|
idns = idns.split(" ")
|
||||||
|
for r in idns:
|
||||||
|
idns_records.append(r)
|
||||||
|
|
||||||
|
for record in idns_records:
|
||||||
|
print(record)
|
||||||
|
type = record.split(":")[0]
|
||||||
|
content = record.split(":")[1]
|
||||||
|
key = content.split("=")[0]
|
||||||
|
value = content.split("=")[1]
|
||||||
|
print(f"Type: {type}, Key: {key}, Value: {value}")
|
||||||
|
if type == "auth" and key == "login.hns.au":
|
||||||
|
if value == uuid:
|
||||||
|
# Add domain to user
|
||||||
|
user = User.query.filter_by(username=domain).first()
|
||||||
|
if not user:
|
||||||
|
# Create user with username and profile picture
|
||||||
|
user = User(username=domain)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
session["id"] = user.id
|
||||||
|
session.permanent = True
|
||||||
|
if "domains" not in session:
|
||||||
|
session["domains"] = []
|
||||||
|
|
||||||
|
if domain not in session["domains"]:
|
||||||
|
session["domains"].append(domain)
|
||||||
|
print("User logged in with TXT")
|
||||||
|
|
||||||
|
# Check if next page is specified
|
||||||
|
next_page = request.args.get("next")
|
||||||
|
print(next_page)
|
||||||
|
if next_page and next_page != "None":
|
||||||
|
return redirect(next_page)
|
||||||
|
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
@bp.route("/txt/<domain>")
|
||||||
|
def txtLoginDomain(domain):
|
||||||
|
# Get uuid
|
||||||
|
uuid = session["uuid"]
|
||||||
|
|
||||||
|
query = dns.message.make_query(domain, dns.rdatatype.TXT)
|
||||||
|
dns_request = query.to_wire()
|
||||||
|
|
||||||
|
# Send the DNS query over HTTPS
|
||||||
|
response = requests.post('https://hnsdoh.com/dns-query', data=dns_request, headers={'Content-Type': 'application/dns-message'})
|
||||||
|
|
||||||
|
# Parse the DNS response
|
||||||
|
dns_response = dns.message.from_wire(response.content)
|
||||||
|
|
||||||
|
# Loop over TXT records and look for profile avatar
|
||||||
|
idns_records = []
|
||||||
|
for record in dns_response.answer:
|
||||||
|
if record.rdtype == dns.rdatatype.TXT:
|
||||||
|
for txt in record:
|
||||||
|
txt_value:str = txt.to_text().strip('"')
|
||||||
|
if txt_value.startswith("IDNS1"):
|
||||||
|
print(txt_value)
|
||||||
|
idns = txt_value.removeprefix("IDNS1 ")
|
||||||
|
idns = idns.split(" ")
|
||||||
|
for r in idns:
|
||||||
|
idns_records.append(r)
|
||||||
|
|
||||||
|
for record in idns_records:
|
||||||
|
print(record)
|
||||||
|
type = record.split(":")[0]
|
||||||
|
content = record.split(":")[1]
|
||||||
|
key = content.split("=")[0]
|
||||||
|
value = content.split("=")[1]
|
||||||
|
print(f"Type: {type}, Key: {key}, Value: {value}")
|
||||||
|
if type == "auth" and key == "login.hns.au":
|
||||||
|
if value == uuid:
|
||||||
|
# Add domain to user
|
||||||
|
user = User.query.filter_by(username=domain).first()
|
||||||
|
if not user:
|
||||||
|
# Create user with username and profile picture
|
||||||
|
user = User(username=domain)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
session["id"] = user.id
|
||||||
|
session.permanent = True
|
||||||
|
if "domains" not in session:
|
||||||
|
session["domains"] = []
|
||||||
|
|
||||||
|
if domain not in session["domains"]:
|
||||||
|
session["domains"].append(domain)
|
||||||
|
print("User logged in with TXT")
|
||||||
|
|
||||||
|
# Check if next page is specified
|
||||||
|
next_page = request.args.get("next")
|
||||||
|
print(next_page)
|
||||||
|
if next_page and next_page != "None":
|
||||||
|
return redirect(next_page)
|
||||||
|
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
@bp.route("/logout")
|
@bp.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
del session["id"]
|
del session["id"]
|
||||||
@ -355,11 +492,13 @@ def avatar(username):
|
|||||||
|
|
||||||
return send_from_directory("templates", "favicon.png", mimetype="image/png")
|
return send_from_directory("templates", "favicon.png", mimetype="image/png")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/favicon.png")
|
@bp.route("/favicon.png")
|
||||||
def favicon():
|
def favicon():
|
||||||
return send_from_directory("templates", "favicon.png", mimetype="image/png")
|
return send_from_directory("templates", "favicon.png", mimetype="image/png")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.errorhandler(404)
|
||||||
|
@bp.app_errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
print(f'404 error: {e}')
|
||||||
|
return render_template("404.html"), 404
|
||||||
|
125
website/templates/404.html
Normal file
125
website/templates/404.html
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>HNS Login</title>
|
||||||
|
<link rel="icon" href="favicon.png" type="image/png">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
/* Dark theme*/
|
||||||
|
background-color: #222;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button {
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.loginbutton {
|
||||||
|
/* Put in the centre of the screen */
|
||||||
|
|
||||||
|
margin-left: 50%;
|
||||||
|
margin-top: 20px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-option {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centre {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>HNS Login</h1>
|
||||||
|
|
||||||
|
<h2>404 - Page Not Found</h2>
|
||||||
|
<a href="/" class="button">Return to Home</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div style="position: fixed; bottom: 0; width: 100%; text-align: center; background-color: #333; padding: 10px;">
|
||||||
|
Powered by <a href="https://auth.varo.domains/implement" target="_blank">Varo Auth</a>, <a href="https://hns.id/"
|
||||||
|
target="_blank">HNS.ID</a> and <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a>
|
||||||
|
</div>
|
||||||
|
<div style="height: 5em;"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -62,6 +62,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@ -77,17 +78,33 @@
|
|||||||
.login-option {
|
.login-option {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 25px;
|
margin-right: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centre {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -118,16 +135,16 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<br>
|
<br>
|
||||||
{% if user.id == 1 %}
|
{% if user.id == 1 %}
|
||||||
{% for user_tmp in users %}
|
{% for user_tmp in users %}
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<strong>User Info</strong>
|
<strong>User Info</strong>
|
||||||
{%- for key in user_tmp %}
|
{%- for key in user_tmp %}
|
||||||
<strong>{{ key }}: </strong>{{ user_tmp[key] }}
|
<strong>{{ key }}: </strong>{{ user_tmp[key] }}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</pre>
|
</pre>
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
@ -135,17 +152,49 @@
|
|||||||
Contact Nathan.Woodburn/ on any social media platform</p>
|
Contact Nathan.Woodburn/ on any social media platform</p>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<h2>Login with your Handshake domain</h2>
|
<h2>Login with your Handshake domain</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
|
||||||
|
|
||||||
<div class="login-option">
|
<div class="login-option">
|
||||||
<script type="text/javascript" src="https://auth.varo.domains/v1"></script>
|
{% if domains %}
|
||||||
<script>var varo = new Varo();</script>
|
<p>Login using a TXT record</p>
|
||||||
<button class="loginbutton" onclick='varo.auth().then(auth => {
|
|
||||||
|
<div style="text-align: center;margin-top: 25px; margin-bottom: 25px;">
|
||||||
|
<select id="TXTDomainDropdown">
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{domain}}">{{domain}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button onclick="TXTLoginSelect()">Login</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function TXTLoginSelect() {
|
||||||
|
var selectedNFT = document.getElementById("TXTDomainDropdown").value;
|
||||||
|
window.location.href = "/txt/" + selectedNFT + window.location.search;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="centre">Login with a new domain?</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="centre">Add this TXT record to any domain or login with an existing domain in Varo Auth</span>
|
||||||
|
<div class="centre">
|
||||||
|
<pre style="display: inline;margin-right: 10px;">IDNS1 auth:login.hns.au={{uuid}}</pre>
|
||||||
|
<!-- Copy button -->
|
||||||
|
<button style="display: inline;" onclick="copyToClipboard('IDNS1 auth:login.hns.au={{uuid}}')">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="centre" style="margin-top: 25px;">
|
||||||
|
<form action="/txt?next={{ next }}" method="post">
|
||||||
|
<input type="text" name="domain" placeholder="Enter your domain">
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript" src="https://auth.varo.domains/v1"></script>
|
||||||
|
<script>var varo = new Varo();</script>
|
||||||
|
<button class="loginbutton" onclick='varo.auth().then(auth => {
|
||||||
if (auth.success) {
|
if (auth.success) {
|
||||||
// handle success by calling your api to update the users session
|
// handle success by calling your api to update the users session
|
||||||
$.post("/", JSON.stringify(auth.data), (response) => {
|
$.post("/", JSON.stringify(auth.data), (response) => {
|
||||||
@ -153,7 +202,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});'>Login with Varo</button>
|
});'>Login with Varo</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="login-option">
|
<div class="login-option">
|
||||||
<!-- Login for HNS.ID domains -->
|
<!-- Login for HNS.ID domains -->
|
||||||
@ -216,42 +265,50 @@
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{% if address %}
|
{% if address %}
|
||||||
<h4 style="text-align: center;">Logged in as {{address}}</h4>
|
|
||||||
|
|
||||||
|
<h4 style="text-align: center;">Logged in with HNS.ID</h4>
|
||||||
<span style="text-align: center;display: block;">Select a HNS.ID domain to log in with</span><br>
|
<span style="text-align: center;display: block;">Select a HNS.ID domain to log in with</span><br>
|
||||||
|
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<select id="nftDropdown">
|
<select id="nftDropdown">
|
||||||
{% for nft in hnsid.nfts %}
|
{% for nft in hnsid.nfts %}
|
||||||
<option value="{{nft.name}}">{{nft.name}}</option>
|
<option value="{{nft.name}}">{{nft.name}}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<button onclick="HNSIDLoginSelect()">Login</button>
|
<button onclick="HNSIDLoginSelect()">Login</button>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
function HNSIDLoginSelect() {
|
function HNSIDLoginSelect() {
|
||||||
var selectedNFT = document.getElementById("nftDropdown").value;
|
var selectedNFT = document.getElementById("nftDropdown").value;
|
||||||
window.location.href = "/hnsid/" + selectedNFT + window.location.search;
|
window.location.href = "/hnsid/" + selectedNFT + window.location.search;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<button class="loginbutton" onclick='javascript:loginETH();'>Login with another ETH address</button>
|
<button class="loginbutton" onclick='javascript:loginETH();'>Login with another ETH address</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="loginbutton" onclick='javascript:loginETH();'>Login with HNS.ID</button>
|
<button class="loginbutton" onclick='javascript:loginETH();'>Login with HNS.ID</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyToClipboard(text) {
|
||||||
|
var dummy = document.createElement("textarea");
|
||||||
|
document.body.appendChild(dummy);
|
||||||
|
dummy.value = text;
|
||||||
|
dummy.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(dummy);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div style="position: fixed; bottom: 0; width: 100%; text-align: center; background-color: #333; padding: 10px;">
|
<div style="position: fixed; bottom: 0; width: 100%; text-align: center; background-color: #333; padding: 10px;">
|
||||||
Powered by <a href="https://auth.varo.domains/implement" target="_blank">Varo Auth</a>, <a href="https://hns.id/" target="_blank">HNS.ID</a> and <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a>
|
Powered by <a href="https://auth.varo.domains/implement" target="_blank">Varo Auth</a>, <a href="https://hns.id/"
|
||||||
|
target="_blank">HNS.ID</a> and <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 5em;"></div>
|
<div style="height: 5em;"></div>
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
Reference in New Issue
Block a user