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 += "
Stats
"
+ html += "Workers
"
+ html += "Number of workers: " + str(len(workers)) + "
"
+ html += "Workers:
"
+ html += ""
+ for worker in workers:
+ html += "- Name: " + worker.split(':')[0] + " | IP " + worker.split(':')[2].strip('\n') + "
"
+ html += "
"
+ html += "Sites
"
+ html += "Number of sites: " + str(len(sites)) + "
"
+ html += "Sites:
"
+ html += ""
+ for site in sites:
+ html += "- Domain: " + site.split(':')[0] + " | Worker: " + site.split(':')[1].strip('\n') + "
"
+ html += "
"
+ html += "Licences
"
+ html += "Number of licences: " + str(len(licences)) + "
"
+
+ html += ""
+ 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 += ""
+ for licence in licences:
+ html += "- " + licence.strip('\n') + "
"
+ 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 += "
"
+ # Form to add site
+ html += "Add site
"
+ 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