diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index c09eabc..a5f1c1e 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -4,6 +4,7 @@ on: push: branches: - '*' + - '*/*' tags-ignore: - '*' diff --git a/README.md b/README.md index 6957d02..def6909 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ General commands (as anyone) Docker is the easiest way to install the master server. ```sh -docker run -d -p 5000:5000 -e LICENCE-API=your-api-key -e WORKER_KEY=your-api-key --name hnshosting-master git.woodburn.au/nathanwoodburn/hnshosting-master:latest -v ./data:/data +docker run -d -p 5000:5000 -e LICENCE_KEY=your-api-key -e WORKER_KEY=your-api-key -e ADMIN_KEY=admin-key --name hnshosting-master git.woodburn.au/nathanwoodburn/hnshosting-master:latest -v ./data:/data ``` You can also mount a docker volume to /data to store the files instead of mounting a host directory. diff --git a/master/main.py b/master/main.py index 45ffc35..426ea7e 100644 --- a/master/main.py +++ b/master/main.py @@ -1,4 +1,4 @@ -from flask import Flask, request, jsonify +from flask import Flask, make_response, redirect, request, jsonify import dotenv import os import requests @@ -10,12 +10,14 @@ dotenv.load_dotenv() app = Flask(__name__) +logins = [] + # API add license key (requires API key in header) @app.route('/add-licence', methods=['POST']) def add_license(): # Get API header api_key = request.headers.get('key') - if api_key != os.getenv('LICENCE-API'): + if api_key != os.getenv('LICENCE_KEY'): return jsonify({'error': 'Invalid API key', 'success': 'false'}) # Generate licence key @@ -180,18 +182,23 @@ def list_workers(): continue online=True - resp=requests.get("http://"+worker.split(':')[1].strip('\n') + ":5000/status",timeout=2) - if (resp.status_code != 200): - online=False - worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[2].strip('\n'), 'online': online, 'sites': 0, 'status': 'offline'}) + try: + resp=requests.get("http://"+worker.split(':')[1].strip('\n') + ":5000/status",timeout=2) + + if (resp.status_code != 200): + online=False + worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[2].strip('\n'), 'online': online, 'sites': 0, 'status': 'offline'}) + continue + sites = resp.json()['num_sites'] + availability = resp.json()['availability'] + if availability == True: + worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[2].strip('\n'), 'online': online, 'sites': sites, 'status': 'ready'}) + else: + worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[2].strip('\n'), 'online': online, 'sites': sites, 'status': 'full'}) + except: + worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[2].strip('\n'), 'online': 'False', 'sites': 0, 'status': 'offline'}) continue - sites = resp.json()['num_sites'] - availability = resp.json()['availability'] - if availability == True: - worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[2].strip('\n'), 'online': online, 'sites': sites, 'status': 'ready'}) - else: - worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[2].strip('\n'), 'online': online, 'sites': sites, 'status': 'full'}) - + if len(worker_list) == 0: return jsonify({'error': 'No workers available', 'success': 'false'}) return jsonify({'success': 'true', 'workers': worker_list}) @@ -226,6 +233,35 @@ def site_status(): return jsonify({'success': 'false', 'domain': domain, 'ip': publicIP, 'tlsa': 'none','error': 'No TLSA record found'}) +@app.route('/info') +def site_status_human(): + domain = request.args.get('domain') + if domain == None: + return "

Invalid domain

" + + # Check if domain exists + if not site_exists(domain): + return "

Domain does not exist

" + + # Get worker + worker = site_worker(domain) + if worker == None: + return "

Domain does not exist

" + + # Get worker ip + ip = workerIP_PRIV(worker) + + # Get TLSA record + resp=requests.get("http://"+ip + ":5000/tlsa?domain=" + domain,timeout=2) + json = resp.json() + publicIP = workerIP(worker) + + if "tlsa" in json: + tlsa = json['tlsa'] + return "

Domain: " + domain + "


IP: " + publicIP + "


TLSA: " + tlsa + "


Make sure to add the TLSA record to `_443._tcp." + domain + "` or `*." + domain + "`

" + else: + return "

Domain: " + domain + "


IP: " + publicIP + "


TLSA: none


No TLSA record found

" + @app.route('/tlsa', methods=['GET']) def tlsa(): domain = request.args.get('domain') @@ -390,6 +426,364 @@ def workerIP(worker): return ip +# Home page +@app.route('/') +def home(): + # Show stats and info + + # Get worker info + workers = [] + try: + workers_file = open('/data/workers.txt', 'r') + workers = workers_file.readlines() + workers_file.close() + except FileNotFoundError: + pass + + # Get site info + sites = [] + try: + sites_file = open('/data/sites.txt', 'r') + sites = sites_file.readlines() + sites_file.close() + except FileNotFoundError: + pass + + # Get licence info + licences = [] + try: + licences_file = open('/data/licence_key.txt', 'r') + licences = licences_file.readlines() + licences_file.close() + except FileNotFoundError: + pass + + # Create html page + html = "

Welcome


" + html += "

Create a site

" + html += "
" + html += "

Domain:

" + html += "

Licence key:

" + html += "" + html += "
" + + html += "

Stats


" + html += "

Workers

" + html += "

Number of workers: " + str(len(workers)) + "

" + html += "

Workers:

" + html += "" + html += "

Sites

" + html += "

Number of sites: " + str(len(sites)) + "

" + html += "

Sites:

" + html += "" + html += "

Licences

" + html += "

Number of licences: " + str(len(licences)) + "

" + + html += "

Admin

" + return html + +# Admin page +@app.route('/admin') +def admin(): + # Check if logged in + login_key = request.cookies.get('login_key') + + if login_key == None: + return "

Admin


" + if login_key not in logins: + return "

Admin


" + + # Show some admin stuff + licences = [] + try: + licences_file = open('/data/licence_key.txt', 'r') + licences = licences_file.readlines() + licences_file.close() + except FileNotFoundError: + pass + + # Create html page + html = "

Admin


" + html += "

Licences

" + html += "

Number of licences: " + str(len(licences)) + "

" + html += "

Licences:

" + html += "" + html += "

API

" + html += "

API key: " + os.getenv('LICENCE_KEY') + "

" + html += "

Worker key: " + os.getenv('WORKER_KEY') + "

" + html += "

Stripe

" + # Check if stripe is enabled + if os.getenv('STRIPE_SECRET') == None: + html += "

Stripe is not enabled

" + else: + html += "

Stripe is enabled

" + + html += "

Workers

" + workers = [] + try: + workers_file = open('/data/workers.txt', 'r') + workers = workers_file.readlines() + workers_file.close() + except FileNotFoundError: + pass + + for worker in workers: + if not worker.__contains__(':'): + continue + + html += "

Name: " + worker.split(':')[0] + " | Public IP " + worker.split(':')[2].strip('\n') + " | Private IP " + worker.split(':')[1] + # Check worker status + online=True + try: + resp=requests.get("http://"+worker.split(':')[1].strip('\n') + ":5000/status",timeout=2) + if (resp.status_code != 200): + html += " | Status: Offline" + else: + html += " | Status: Online | Sites: " + str(resp.json()['num_sites']) + " | Availability: " + str(resp.json()['availability']) + except: + html += " | Status: Offline" + + html += "

" + + + html += "

Sites

" + sites = [] + try: + sites_file = open('/data/sites.txt', 'r') + sites = sites_file.readlines() + sites_file.close() + except FileNotFoundError: + pass + + for site in sites: + if not site.__contains__(':'): + continue + domain = site.split(':')[0] + html += "

Domain: " + domain + " | Worker: " + site.split(':')[1].strip('\n') + " | Info

" + + html += "

" + # Form to add worker + html += "

Add worker

" + html += "
" + html += "

Name:

" + html += "

Public IP:

" + html += "

Private IP:

" + html += "" + html += "
" + + html += "

Add Licence


" + # Form to add site + html += "

Add site

" + html += "
" + html += "

Domain:

" + html += "" + html += "
" + + + html += "
Logout" + + + return html + + +@app.route('/add-site', methods=['POST']) +def addsite(): + # Check for licence key + if 'licence' not in request.form: + # Check cookie + login_key = request.cookies.get('login_key') + if login_key == None: + return redirect('/admin') + if login_key not in logins: + return redirect('/admin') + else: + # Use licence key + licence_key = request.form['licence'] + # Check if licence key is valid + key_file = open('/data/licence_key.txt', 'r') + valid_key = False + for line in key_file.readlines(): + if licence_key == line.strip('\n'): + valid_key = True + break + key_file.close() + if not valid_key: + return jsonify({'error': 'Invalid licence', 'success': 'false'}) + + # Delete licence key + key_file = open('/data/licence_key.txt', 'r') + lines = key_file.readlines() + key_file.close() + key_file = open('/data/licence_key.txt', 'w') + for line in lines: + if line.strip("\n") != licence_key: + key_file.write(line) + key_file.close() + + # Get domain + domain = request.form['domain'] + if domain == None: + return jsonify({'error': 'No domain sent', 'success': 'false'}) + # Check if domain already exists + if site_exists(domain): + return jsonify({'error': 'Domain already exists', 'success': 'false'}) + + # Check if domain contains http:// or https:// + if domain.startswith("http://") or domain.startswith("https://"): + return jsonify({'error': 'Domain should not contain http:// or https://', 'success': 'false'}) + + + # Check if worker file exists + workers = None + try: + worker_file = open('/data/workers.txt', 'r') + workers = worker_file.readlines() + worker_file.close() + except FileNotFoundError: + return jsonify({'error': 'No workers available', 'success': 'false'}) + + # Get a worker that has available slots + worker = None + for line in workers: + if not line.__contains__(':'): + continue + + ip = line.split(':')[1].strip('\n') + resp=requests.get("http://"+ip + ":5000/status",timeout=2) + if (resp.status_code == 200): + if resp.json()['availability'] == True: + worker = line + break + + if worker == None: + return jsonify({'error': 'No workers available', 'success': 'false'}) + + + # Add domain to file + sites_file = open('/data/sites.txt', 'a') + sites_file.write(domain + ':' + worker.split(':')[0] + '\n') + sites_file.close() + + # Send worker request + requests.post("http://"+ worker.split(':')[1].strip('\n') + ":5000/new-site?domain=" + domain) + + html = "

Site creating...


" + html += "

Domain: " + domain + "

" + html += "

Worker: " + worker.split(':')[0] + "

" + html += "

Worker IP: " + worker.split(':')[1].strip('\n') + "

" + html += "

Check status

" + + return html + + +@app.route('/licence') +def licence(): + # Check cookie + login_key = request.cookies.get('login_key') + if login_key == None: + return redirect('/admin') + if login_key not in logins: + return redirect('/admin') + + licence_key = os.urandom(16).hex() + + # Add license key to file + key_file = open('/data/licence_key.txt', 'a') + key_file.write(licence_key + '\n') + key_file.close() + + return "

Licence key


" + licence_key + "


Back" + + + +@app.route('/new-worker', methods=['POST']) +def new_worker(): + # Check cookie + login_key = request.cookies.get('login_key') + + if login_key == None: + return redirect('/admin') + if login_key not in logins: + return redirect('/admin') + + worker = request.form['name'] + worker_IP = request.form['ip'] + worker_PRIV = request.form['priv'] + + + # Check worker file + try: + workers_file = open('/data/workers.txt', 'r') + except FileNotFoundError: + workers_file = open('/data/workers.txt', 'w') + workers_file.close() + workers_file = open('/data/workers.txt', 'r') + + # Check if worker already exists + if worker in workers_file.read(): + return jsonify({'error': 'Worker already exists', 'success': 'false'}) + + workers_file.close() + + # Add worker to file + workers_file = open('/data/workers.txt', 'a') + workers_file.write(worker + ":" + worker_PRIV + ":"+ worker_IP + '\n') + workers_file.close() + + return redirect('/admin') + + +@app.route('/logout') +def logout(): + login_key = request.cookies.get('login_key') + if login_key == None: + return redirect('/admin') + if login_key not in logins: + return redirect('/admin') + + logins.remove(login_key) + return redirect('/admin') + + +@app.route('/login', methods=['POST']) +def login(): + # Handle login + print('Login attempt', flush=True) + # Check if form contains password + if 'password' not in request.form: + print('Login failed', flush=True) + return redirect('/failed-login') + + password = request.form['password'] + if os.getenv('ADMIN_KEY') == password: + print('Login success', flush=True) + # Generate login key + login_key = os.urandom(32).hex() + logins.append(login_key) + # Set cookie + resp = make_response(redirect('/admin')) + resp.set_cookie('login_key', login_key) + return resp + print('Login failed', flush=True) + return redirect('/failed-login') + +@app.route('/failed-login') +def failed_login(): + return "

Failed login


" + + + + + # Start the server if __name__ == '__main__': app.run(debug=False, port=5000, host='0.0.0.0') \ No newline at end of file