diff --git a/bin/cmd b/bin/cmd index ea97b1f..2d248f4 100755 --- a/bin/cmd +++ b/bin/cmd @@ -1,7 +1,7 @@ #!/bin/bash source .venv/bin/activate -export FLASK_APP=nodes/app.py +export FLASK_APP=xmrnodes/app.py export FLASK_SECRETS=config.py export FLASK_DEBUG=1 flask $1 diff --git a/xmrnodes/app.py b/xmrnodes/app.py index 646a505..ca3281d 100644 --- a/xmrnodes/app.py +++ b/xmrnodes/app.py @@ -1,19 +1,28 @@ import json import requests import re +import logging from os import makedirs +from datetime import datetime from flask import Flask, request, redirect from flask import render_template, flash, url_for from urllib.parse import urlparse +from xmrnodes.forms import SubmitNode from xmrnodes.models import Node +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) + app = Flask(__name__) app.config.from_envvar("FLASK_SECRETS") app.secret_key = app.config["SECRET_KEY"] @app.route("/", methods=["GET", "POST"]) def index(): + form = SubmitNode() itp = 20 page = request.args.get("page", 1) try: @@ -22,61 +31,85 @@ def index(): flash("Wow, wtf hackerman. Cool it.") page = 1 - nodes = Node.select().where(Node.available==True).order_by(Node.datetime_entered.desc()).paginate(page, itp) + nodes = Node.select().where(Node.available==True).order_by( + Node.datetime_entered.desc() + ).paginate(page, itp) total_pages = Node.select().count() / itp - return render_template("index.html", nodes=nodes, page=page, total_pages=total_pages) + return render_template( + "index.html", + nodes=nodes, + page=page, + total_pages=total_pages, + form=form + ) @app.route("/add", methods=["GET", "POST"]) def add(): if request.method == "POST": - url = request.form.get("url") + url = request.form.get("node_url") regex = re.compile( - r'^(?:http)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip - r'(?::\d+)?' # optional port - r'(?:/?|[/?]\S+)$', re.IGNORECASE) - - re_match = re.match(regex, url) - - if re_match is not None: - _url = urlparse(url) - try: - endpoint = f"{_url.scheme}://{_url.netloc}" - r = requests.get(endpoint + "/get_info", timeout=3) - r.raise_for_status() - # print(r.json()) - return {"status": "success"} - except requests.exceptions.ConnectTimeout: - flash("connection timed out. double-check the port") - return {"status": "fail", "reason": "timeout"} - except requests.exceptions.SSLError: - flash("invalid certificate") - return {"status": "fail", "reason": "invalid cert"} - except Exception as e: - flash("failed to send req", str(e)) - print(e) - return {"status": "fail"} - else: - flash("invalid url provided") - return {"status": "fail"} - - return "ok" - node = Node( - scheme=proto, - address=addr, - port=port, - version=r.json()["version"], - tor=addr.endswith(".onion"), - available=r.json()["status"] == "OK", - mainnet=r.json()["mainnet"], + r'^(?:http)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... + r'localhost|' #localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE ) - node.save() - return {"status": "success"} + re_match = re.match(regex, url) + if re_match is None: + flash("This doesn't look like a valid URL") + else: + _url = urlparse(url) + url = f"{_url.scheme}://{_url.netloc}" + if Node.select().where(Node.url == url).exists(): + flash("This node is already in the database.") + else: + flash("Seems like a valid node. Added to the database and will check soon.") + node = Node(url=url) + node.save() return redirect("/") +@app.cli.command("validate") +def validate(): + nodes = Node.select().where(Node.validated == False) + for node in nodes: + now = datetime.now() + is_onion = node.url.split(":")[1].endswith(".onion") + logging.info(f"Attempting to validate {node.url}") + if is_onion: + logging.info("onion address found") + node.tor = True + try: + r = requests.get(node.url + "/get_info", timeout=3) + r.raise_for_status() + assert "height" in r.json() + assert "nettype" in r.json() + nettype = r.json()["nettype"] + logging.info("success") + if nettype in ["mainnet", "stagenet", "testnet"]: + node.nettype = nettype + node.available = True + node.validated = True + node.datetime_checked = now + node.save() + else: + logging.info("unexpected nettype") + except requests.exceptions.ConnectTimeout: + logging.info("connection timed out") + node.delete_instance() + except requests.exceptions.SSLError: + logging.info("invalid certificate") + node.delete_instance() + except requests.exceptions.ConnectionError: + logging.info("connection error") + node.delete_instance() + except requests.exceptions.HTTPError: + logging.info("http error, 4xx or 5xx") + node.delete_instance() + except Exception as e: + logging.info("failed for reasons unknown") + node.delete_instance() @app.route("/about") def about(): diff --git a/xmrnodes/forms.py b/xmrnodes/forms.py new file mode 100644 index 0000000..80fa413 --- /dev/null +++ b/xmrnodes/forms.py @@ -0,0 +1,7 @@ +from flask_wtf import FlaskForm +from wtforms import StringField +from wtforms.validators import DataRequired + + +class SubmitNode(FlaskForm): + node_url = StringField('Node URL:', validators=[DataRequired()]) diff --git a/xmrnodes/models.py b/xmrnodes/models.py index 8e6d3b7..af0a317 100644 --- a/xmrnodes/models.py +++ b/xmrnodes/models.py @@ -4,19 +4,17 @@ from xmrnodes import config data_dir = getattr(config, 'DATA_FOLDER', './data') -db = SqliteDatabase(f"{data_dir}/db/sqlite.db") +db = SqliteDatabase(f"{data_dir}/sqlite.db") class Node(Model): id = AutoField() - scheme = CharField() - address = CharField() - port = IntegerField() - version = CharField(null=True) + url = CharField() tor = BooleanField(default=False) available = BooleanField(default=False) - mainnet = BooleanField(default=False) + validated = BooleanField(default=False) + nettype = CharField(null=True) datetime_entered = DateTimeField(default=datetime.now) - datetime_checked = DateTimeField(default=datetime.now) + datetime_checked = DateTimeField(default=None, null=True) datetime_failed = DateTimeField(default=None, null=True) class Meta: diff --git a/xmrnodes/templates/index.html b/xmrnodes/templates/index.html index 6b01b04..993bced 100644 --- a/xmrnodes/templates/index.html +++ b/xmrnodes/templates/index.html @@ -9,13 +9,25 @@ {% for node in nodes %} - {{ node }}
+ {{ node.url }}
{% endfor %} -
- - - + + {{ form.csrf_token }} + {% for f in form %} + {% if f.name != 'csrf_token' %} +
+ {{ f.label }} + {{ f }} +
+ {% endif %} + {% endfor %} + +