Compare commits
59 Commits
afd5286ef5
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
67ed81fd2e
|
|||
|
2ef8e2c38d
|
|||
|
c8eafb6406
|
|||
|
73d11a0f76
|
|||
|
3628e0a4ac
|
|||
|
20dbf6c0df
|
|||
|
8a2cad16ec
|
|||
|
48330f7ef9
|
|||
|
7703bfa4d5
|
|||
|
ba96f9b84b
|
|||
|
0a33a6150d
|
|||
|
c9be5cedcb
|
|||
|
1fb2493848
|
|||
|
9853214d83
|
|||
|
843d2d12a0
|
|||
|
8f962804a4
|
|||
|
9e485265af
|
|||
|
83bde4b218
|
|||
|
52fca38af9
|
|||
|
4db44bb99e
|
|||
|
0e2ad55eb7
|
|||
|
0c20572369
|
|||
|
d2e31bb684
|
|||
|
bbe70647d7
|
|||
|
d6dffc0464
|
|||
|
fb9295c260
|
|||
|
c4ae4561e3
|
|||
|
2902624637
|
|||
|
5049796d07
|
|||
|
5efe4860fc
|
|||
|
499a7e348b
|
|||
|
ed94263050
|
|||
|
4841344d63
|
|||
|
51bcdda5d4
|
|||
|
b38de6ad52
|
|||
|
b642cf7269
|
|||
|
3e3c2fe61e
|
|||
|
04edb8b456
|
|||
|
45c1ea3557
|
|||
|
778c1b3d92
|
|||
|
19806b7b1b
|
|||
|
908a4e0422
|
|||
|
d217309e74
|
|||
|
ce8827ed97
|
|||
|
3b914abf7a
|
|||
|
3266dbafa9
|
|||
|
d7c6e1cf70
|
|||
|
dbbb60cab8
|
|||
|
f54d805371
|
|||
|
b56ece2216
|
|||
|
6eef78a48f
|
|||
|
7cddc059b5
|
|||
|
dd4d97ffc9
|
|||
|
6d28cf7431
|
|||
|
287567a513
|
|||
|
8e9055dcd3
|
|||
|
0383b47b8e
|
|||
|
37158f410e
|
|||
|
fd3e9ba760
|
@@ -1,6 +1,12 @@
|
||||
name: Build Docker
|
||||
run-name: Build Docker Images
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
tags-ignore:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
Build Master:
|
||||
@@ -21,11 +27,13 @@ jobs:
|
||||
run : |
|
||||
cd master
|
||||
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
|
||||
tag_num=$(git rev-parse --short HEAD)
|
||||
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
|
||||
tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
tag=${tag//\//-}
|
||||
if [ tag = "main" ]; then
|
||||
tag_num=${GITHUB_RUN_NUMBER}
|
||||
echo "tag_num=$tag_num"
|
||||
|
||||
if [[ "$tag" == "main" ]]; then
|
||||
tag="latest"
|
||||
else
|
||||
tag_num="${tag}-${tag_num}"
|
||||
@@ -60,11 +68,11 @@ jobs:
|
||||
run : |
|
||||
cd discord-bot
|
||||
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
|
||||
tag_num=$(git rev-parse --short HEAD)
|
||||
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
|
||||
tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
tag=${tag//\//-}
|
||||
if [ tag = "main" ]; then
|
||||
tag_num=${GITHUB_RUN_NUMBER}
|
||||
if [[ "$tag" == "main" ]]; then
|
||||
tag="latest"
|
||||
else
|
||||
tag_num="${tag}-${tag_num}"
|
||||
@@ -74,5 +82,4 @@ jobs:
|
||||
docker tag hnshosting-bot:$tag_num git.woodburn.au/nathanwoodburn/hnshosting-bot:$tag_num
|
||||
docker push git.woodburn.au/nathanwoodburn/hnshosting-bot:$tag_num
|
||||
docker tag hnshosting-bot:$tag_num git.woodburn.au/nathanwoodburn/hnshosting-bot:$tag
|
||||
docker push git.woodburn.au/nathanwoodburn/hnshosting-bot:$tag
|
||||
|
||||
docker push git.woodburn.au/nathanwoodburn/hnshosting-bot:$tag
|
||||
60
.gitea/workflows/release.yml
Normal file
60
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Build Docker for Release
|
||||
run-name: Build Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
Build Master:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Docker
|
||||
run : |
|
||||
apt-get install ca-certificates curl gnupg
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
apt-get update
|
||||
apt-get install docker-ce-cli -y
|
||||
- name: Build Docker image
|
||||
run : |
|
||||
cd master
|
||||
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
|
||||
tag=${GITHUB_REF#refs/tags/}
|
||||
|
||||
docker build -t hnshosting-master:release-$tag .
|
||||
docker tag hnshosting-master:release-$tag git.woodburn.au/nathanwoodburn/hnshosting-master:release-$tag
|
||||
docker push git.woodburn.au/nathanwoodburn/hnshosting-master:release-$tag
|
||||
docker tag hnshosting-master:release-$tag git.woodburn.au/nathanwoodburn/hnshosting-master:release
|
||||
docker push git.woodburn.au/nathanwoodburn/hnshosting-master:release
|
||||
|
||||
Build Bot:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Docker
|
||||
run : |
|
||||
apt-get install ca-certificates curl gnupg
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
apt-get update
|
||||
apt-get install docker-ce-cli -y
|
||||
- name: Build Docker image
|
||||
run : |
|
||||
cd discord-bot
|
||||
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
|
||||
tag=${GITHUB_REF#refs/tags/}
|
||||
|
||||
docker build -t hnshosting-bot:release-$tag .
|
||||
docker tag hnshosting-bot:release-$tag git.woodburn.au/nathanwoodburn/hnshosting-bot:release-$tag
|
||||
docker push git.woodburn.au/nathanwoodburn/hnshosting-bot:release-$tag
|
||||
docker tag hnshosting-bot:release-$tag git.woodburn.au/nathanwoodburn/hnshosting-bot:release
|
||||
docker push git.woodburn.au/nathanwoodburn/hnshosting-bot:release
|
||||
35
README.md
35
README.md
@@ -11,12 +11,24 @@ The master server will be used to manage the worker servers.
|
||||
The worker servers will be used to host the wordpress sites.
|
||||
The bot will be used to provide an easier way to manage the master server.
|
||||
|
||||

|
||||
|
||||
| Legend | Description |
|
||||
| --- | --- |
|
||||
| Red Connections | Secured by VPN or over LAN ONLY. (NOT API SECURED) |
|
||||
| Yellow Connections | HTTP/HTTPS public traffic |
|
||||
|
||||
## Features
|
||||
- [x] Add new worker server to master server pool
|
||||
- [x] Create wordpress site on random worker server
|
||||
- [x] Optional Free mode (see Bot section)
|
||||
|
||||
## Usage
|
||||
|
||||
After installing the master and discord bot you can use the following commands (as bot owner).
|
||||
|
||||
```
|
||||
/addworker <ip> <name> | add a worker to the master server pool (Make sure the master can access port 5000 on the worker, and don't allow anyone else to access it)
|
||||
/addworker <ip> <private ip> <name> | add a worker to the master server pool
|
||||
/listworkers | list all workers
|
||||
/licence | Creates a licence key (valid for 1 wp site)
|
||||
```
|
||||
@@ -35,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.
|
||||
|
||||
@@ -74,15 +86,22 @@ cd hnshosting-wp/worker
|
||||
chmod +x install.sh
|
||||
./install.sh
|
||||
```
|
||||
Then to start the worker api server
|
||||
Just press enter when it shows any prompts.
|
||||
|
||||
Start the worker api server using
|
||||
```sh
|
||||
screen -dmS hnshosting-worker python3 main.py
|
||||
```
|
||||
|
||||
Add worker to master server:
|
||||
Add worker to master server pool:
|
||||
The master server will need to be able to access port 5000 on the worker server over the PRIVATE ip. This is not secured by the api key so make sure you don't allow anyone else to access it.
|
||||
|
||||
```sh
|
||||
curl -X POST http://master-server-ip:5000/add-worker?worker=worker-name&ip=worker-server-ip -H "key: api-key"
|
||||
curl -X POST http://master-server-ip:5000/add-worker?worker=worker-name&ip=worker-server-ip&priv=worker-server-private-ip -H "key: api-key"
|
||||
```
|
||||
Alternatively you can use the discord bot to add the worker to the master server pool.
|
||||
```
|
||||
/addworker <ip> <private ip> <name>
|
||||
```
|
||||
|
||||
## Discord bot install
|
||||
@@ -90,4 +109,10 @@ curl -X POST http://master-server-ip:5000/add-worker?worker=worker-name&ip=worke
|
||||
Docker install
|
||||
```sh
|
||||
docker run -d -e MASTER_IP=<MASTER SERVER IP> -e DISCORD_TOKEN=<YOUR-BOT-TOKEN> -e LICENCE_KEY=your-api-key -e WORKER_KEY=your-api-key --name hnshosting-bot git.woodburn.au/nathanwoodburn/hnshosting-bot:latest
|
||||
```
|
||||
|
||||
Enable the free mode by setting the following environment variable.
|
||||
This will allow you to create a wordpress site without using a licence key using the /createsite command.
|
||||
```
|
||||
FREE_MODE: true
|
||||
```
|
||||
BIN
assets/overview.png
Normal file
BIN
assets/overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
@@ -3,6 +3,7 @@ from dotenv import load_dotenv
|
||||
import discord
|
||||
from discord import app_commands
|
||||
import requests
|
||||
import asyncio
|
||||
|
||||
load_dotenv()
|
||||
TOKEN = os.getenv('DISCORD_TOKEN')
|
||||
@@ -12,18 +13,23 @@ Master_Port = os.getenv('MASTER_PORT')
|
||||
if Master_Port == None:
|
||||
Master_Port = "5000"
|
||||
|
||||
FREE_LICENCE = os.getenv('FREE_LICENCE')
|
||||
FREE_LICENCE = os.getenv('FREE_MODE')
|
||||
if FREE_LICENCE == None:
|
||||
FREE_LICENCE = False
|
||||
else:
|
||||
if FREE_LICENCE.lower() == "true":
|
||||
FREE_LICENCE = True
|
||||
else:
|
||||
FREE_LICENCE = False
|
||||
|
||||
intents = discord.Intents.default()
|
||||
client = discord.Client(intents=intents)
|
||||
tree = app_commands.CommandTree(client)
|
||||
|
||||
@tree.command(name="addworker", description="Adds a worker to the master server")
|
||||
async def addworker(ctx, ip: str, name: str):
|
||||
async def addworker(ctx, ip: str,privateip: str, name: str):
|
||||
if ctx.user.id == ADMINID:
|
||||
r = requests.post(f"http://{Master_IP}:{Master_Port}/add-worker?worker={name}&ip={ip}",headers={"key":os.getenv('WORKER_KEY')})
|
||||
r = requests.post(f"http://{Master_IP}:{Master_Port}/add-worker?worker={name}&ip={ip}&priv={privateip}",headers={"key":os.getenv('WORKER_KEY')})
|
||||
if r.status_code == 200:
|
||||
await ctx.response.send_message(f"Worker {name} added to the master server",ephemeral=True)
|
||||
else:
|
||||
@@ -49,9 +55,8 @@ async def listworkers(ctx):
|
||||
@tree.command(name="licence", description="Gets a licence key")
|
||||
async def license(ctx):
|
||||
if ctx.user.id != ADMINID:
|
||||
if FREE_LICENCE == False: # If free licences are enabled then anyone can get a licence
|
||||
await ctx.response.send_message("You do not have permission to use this command",ephemeral=True)
|
||||
return
|
||||
await ctx.response.send_message("You do not have permission to use this command",ephemeral=True)
|
||||
return
|
||||
|
||||
r = requests.post(f"http://{Master_IP}:{Master_Port}/add-licence",headers={"key":os.getenv('LICENCE_KEY')})
|
||||
if r.status_code == 200:
|
||||
@@ -64,12 +69,37 @@ async def license(ctx):
|
||||
await ctx.response.send_message(f"Error getting license\n" + r.text,ephemeral=True)
|
||||
|
||||
@tree.command(name="createsite", description="Create a new WordPress site")
|
||||
async def createsite(ctx, domain: str, licence: str):
|
||||
async def createsite(ctx, domain: str, licence: str = None):
|
||||
if FREE_LICENCE == True: # If free licences are enabled then auto generate a licence
|
||||
r = requests.post(f"http://{Master_IP}:{Master_Port}/add-licence",headers={"key":os.getenv('LICENCE_KEY')})
|
||||
if r.status_code == 200:
|
||||
json = r.json()
|
||||
if json['success'] == "true":
|
||||
licence = json['licence_key']
|
||||
else:
|
||||
await ctx.response.send_message(f"Error getting license\n" + json['error'])
|
||||
return
|
||||
|
||||
r = requests.post(f"http://{Master_IP}:{Master_Port}/new-site?domain={domain}",headers={"key":licence})
|
||||
if r.status_code == 200:
|
||||
json = r.json()
|
||||
if json['success'] == "true":
|
||||
await ctx.response.send_message(f"Site {domain} creating...\nPlease wait a few minutes and then send /siteinfo domain:{domain}")
|
||||
await ctx.response.send_message(f"Site {domain} creating...\nI'll send you a message when it's ready")
|
||||
|
||||
ready = False
|
||||
while ready == False:
|
||||
ready = await check_site_ready(domain)
|
||||
if ready == False:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
r = requests.get(f"http://{Master_IP}:{Master_Port}/site-info?domain={domain}")
|
||||
json = r.json()
|
||||
if json['success'] == "true":
|
||||
await ctx.user.send(f"Site {domain} is ready!\nHere is the site info for {json['domain']}\nA: `{json['ip']}`\nTLSA: `{json['tlsa']}`\nMake sure you put the TLSA in either `_443._tcp.{domain}` or `*.{domain}`")
|
||||
else:
|
||||
await ctx.user.send(f"Error getting site info\n" + json['error'])
|
||||
|
||||
|
||||
else:
|
||||
await ctx.response.send_message(f"Error creating site\n" + json['error'])
|
||||
else:
|
||||
@@ -88,6 +118,17 @@ async def siteinfo(ctx, domain: str):
|
||||
else:
|
||||
await ctx.response.send_message(f"Error getting site info\n" + r.text)
|
||||
|
||||
async def check_site_ready(domain):
|
||||
r = requests.get(f"http://{Master_IP}:{Master_Port}/site-info?domain={domain}")
|
||||
if r.status_code == 200:
|
||||
json = r.json()
|
||||
if json['success'] == "true":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
# When the bot is ready
|
||||
@client.event
|
||||
async def on_ready():
|
||||
|
||||
469
master/main.py
469
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
|
||||
@@ -37,14 +39,22 @@ def new_site():
|
||||
|
||||
# Verify both API key and domain exist
|
||||
if api_key == None:
|
||||
return jsonify({'error': 'Missing API key', 'success': 'false'})
|
||||
return jsonify({'error': 'No licence provided', 'success': 'false'})
|
||||
|
||||
if domain == None:
|
||||
return jsonify({'error': 'Missing domain', 'success': 'false'})
|
||||
|
||||
# Check if API key is a valid site key
|
||||
if api_key not in open('/data/licence_key.txt', 'r').read():
|
||||
return jsonify({'error': 'Invalid API key', 'success': 'false'})
|
||||
key_file = open('/data/licence_key.txt', 'r')
|
||||
valid_key = False
|
||||
for line in key_file.readlines():
|
||||
if api_key == line.strip('\n'):
|
||||
valid_key = True
|
||||
break
|
||||
key_file.close()
|
||||
if not valid_key:
|
||||
return jsonify({'error': 'Invalid licence', 'success': 'false'})
|
||||
|
||||
|
||||
# Check if domain already exists
|
||||
if site_exists(domain):
|
||||
@@ -107,9 +117,10 @@ def new_site():
|
||||
def add_worker():
|
||||
worker=request.args.get('worker')
|
||||
worker_IP=request.args.get('ip')
|
||||
worker_PRIV=request.args.get('priv')
|
||||
# Get API header
|
||||
api_key = request.headers.get('key')
|
||||
if api_key == None or worker == None or worker_IP == None:
|
||||
if api_key == None or worker == None or worker_IP == None or worker_PRIV == None:
|
||||
return jsonify({'error': 'Invalid API key or worker info', 'success': 'false'})
|
||||
if api_key != os.getenv('WORKER_KEY'):
|
||||
return jsonify({'error': 'Invalid API key', 'success': 'false'})
|
||||
@@ -130,11 +141,11 @@ def add_worker():
|
||||
|
||||
# Add worker to file
|
||||
workers_file = open('/data/workers.txt', 'a')
|
||||
workers_file.write(worker + ":" + worker_IP + '\n')
|
||||
workers_file.write(worker + ":" + worker_PRIV + ":"+ worker_IP + '\n')
|
||||
workers_file.close()
|
||||
|
||||
online=True
|
||||
resp=requests.get("http://"+worker_IP + ":5000/ping",timeout=2)
|
||||
resp=requests.get("http://"+worker_PRIV + ":5000/ping",timeout=2)
|
||||
if (resp.status_code != 200):
|
||||
online=False
|
||||
|
||||
@@ -168,17 +179,28 @@ def list_workers():
|
||||
for worker in workers:
|
||||
# Check worker status
|
||||
if not worker.__contains__(':'):
|
||||
return jsonify({'error': 'No workers available', 'success': 'false'})
|
||||
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(':')[1].strip('\n'), 'online': online, 'sites': 0, 'ready': 0})
|
||||
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']
|
||||
worker_list.append({'worker': worker.split(':')[0],'ip': worker.split(':')[1].strip('\n'), 'online': online, 'sites': sites, 'ready': 1})
|
||||
|
||||
|
||||
if len(worker_list) == 0:
|
||||
return jsonify({'error': 'No workers available', 'success': 'false'})
|
||||
return jsonify({'success': 'true', 'workers': worker_list})
|
||||
|
||||
@app.route('/site-info', methods=['GET'])
|
||||
@@ -197,19 +219,49 @@ def site_status():
|
||||
return jsonify({'error': 'Domain does not exist', 'success': 'false'})
|
||||
|
||||
# Get worker ip
|
||||
ip = workerIP(worker)
|
||||
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 jsonify({'success': 'true', 'domain': domain, 'ip': ip, 'tlsa': tlsa})
|
||||
return jsonify({'success': 'true', 'domain': domain, 'ip': publicIP, 'tlsa': tlsa})
|
||||
else:
|
||||
return jsonify({'success': 'false', 'domain': domain, 'ip': ip, 'tlsa': 'none','error': 'No TLSA record found'})
|
||||
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 "<h1>Invalid domain</h1>"
|
||||
|
||||
# Check if domain exists
|
||||
if not site_exists(domain):
|
||||
return "<h1>Domain does not exist</h1>"
|
||||
|
||||
# Get worker
|
||||
worker = site_worker(domain)
|
||||
if worker == None:
|
||||
return "<h1>Domain does not exist</h1>"
|
||||
|
||||
# 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 "<h1>Domain: " + domain + "</h1><br><p>IP: " + publicIP + "</p><br><p>TLSA: " + tlsa + "</p><br><p>Make sure to add the TLSA record to `_443._tcp." + domain + "` or `*." + domain + "`</p>"
|
||||
else:
|
||||
return "<h1>Domain: " + domain + "</h1><br><p>IP: " + publicIP + "</p><br><p>TLSA: none</p><br><p>No TLSA record found</p>"
|
||||
|
||||
@app.route('/tlsa', methods=['GET'])
|
||||
def tlsa():
|
||||
domain = request.args.get('domain')
|
||||
@@ -226,7 +278,7 @@ def tlsa():
|
||||
return jsonify({'error': 'Domain does not exist', 'success': 'false'})
|
||||
|
||||
# Get worker ip
|
||||
ip = workerIP(worker)
|
||||
ip = workerIP_PRIV(worker)
|
||||
|
||||
# Get TLSA record
|
||||
resp=requests.get("http://"+ip + ":5000/tlsa?domain=" + domain,timeout=2)
|
||||
@@ -337,6 +389,24 @@ def site_worker(domain):
|
||||
sites_file.close()
|
||||
return worker
|
||||
|
||||
def workerIP_PRIV(worker):
|
||||
# If file doesn't exist, create it
|
||||
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')
|
||||
|
||||
ip = None
|
||||
for line in workers_file.readlines():
|
||||
if worker == line.split(':')[0]:
|
||||
ip = line.split(':')[2].strip('\n')
|
||||
break
|
||||
|
||||
workers_file.close()
|
||||
return ip
|
||||
|
||||
def workerIP(worker):
|
||||
# If file doesn't exist, create it
|
||||
try:
|
||||
@@ -354,9 +424,366 @@ def workerIP(worker):
|
||||
|
||||
workers_file.close()
|
||||
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 = "<h1>Welcome</h1><br>"
|
||||
html += "<h2>Create a site</h2>"
|
||||
html += "<form action='/add-site' method='POST'>"
|
||||
html += "<p>Domain: <input type='text' name='domain'></p>"
|
||||
html += "<p>Licence key: <input type='text' name='licence'></p>"
|
||||
html += "<input type='submit' value='Create site'>"
|
||||
html += "</form>"
|
||||
|
||||
html += "<br><h2>Stats</h2><br>"
|
||||
html += "<h2>Workers</h2>"
|
||||
html += "<p>Number of workers: " + str(len(workers)) + "</p>"
|
||||
html += "<p>Workers:</p>"
|
||||
html += "<ul>"
|
||||
for worker in workers:
|
||||
html += "<li>Name: " + worker.split(':')[0] + " | IP " + worker.split(':')[2].strip('\n') + "</li>"
|
||||
html += "</ul>"
|
||||
html += "<h2>Sites</h2>"
|
||||
html += "<p>Number of sites: " + str(len(sites)) + "</p>"
|
||||
html += "<p>Sites:</p>"
|
||||
html += "<ul>"
|
||||
for site in sites:
|
||||
html += "<li>Domain: " + site.split(':')[0] + " | Worker: " + site.split(':')[1].strip('\n') + "</li>"
|
||||
html += "</ul>"
|
||||
html += "<h2>Licences</h2>"
|
||||
html += "<p>Number of licences: " + str(len(licences)) + "</p>"
|
||||
|
||||
html += "<h2><a href='/admin'>Admin</a></h2>"
|
||||
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 "<h1>Admin</h1><br><form action='/login' method='POST'><input type='password' name='password'><input type='submit' value='Login'></form>"
|
||||
if login_key not in logins:
|
||||
return "<h1>Admin</h1><br><form action='/login' method='POST'><input type='password' name='password'><input type='submit' value='Login'></form>"
|
||||
|
||||
# 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 = "<h1>Admin</h1><br>"
|
||||
html += "<h2>Licences</h2>"
|
||||
html += "<p>Number of licences: " + str(len(licences)) + "</p>"
|
||||
html += "<p>Licences:</p>"
|
||||
html += "<ul>"
|
||||
for licence in licences:
|
||||
html += "<li>" + licence.strip('\n') + "</li>"
|
||||
html += "</ul>"
|
||||
html += "<h2>API</h2>"
|
||||
html += "<p>API key: " + os.getenv('LICENCE_KEY') + "</p>"
|
||||
html += "<p>Worker key: " + os.getenv('WORKER_KEY') + "</p>"
|
||||
html += "<h2>Stripe</h2>"
|
||||
# Check if stripe is enabled
|
||||
if os.getenv('STRIPE_SECRET') == None:
|
||||
html += "<p>Stripe is not enabled</p>"
|
||||
else:
|
||||
html += "<p>Stripe is enabled</p>"
|
||||
|
||||
html += "<br><br><h2>Workers</h2>"
|
||||
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 += "<p>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 += "</p>"
|
||||
|
||||
|
||||
html += "<h2>Sites</h2>"
|
||||
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 += "<p>Domain: <a href='https://"+ domain + "'>" + domain + "</a> | Worker: " + site.split(':')[1].strip('\n') + " | <a href='/info?domain=" + domain + "'>Info</a></p>"
|
||||
|
||||
html += "<br><br>"
|
||||
# Form to add worker
|
||||
html += "<h2>Add worker</h2>"
|
||||
html += "<form action='/new-worker' method='POST'>"
|
||||
html += "<p>Name: <input type='text' name='name'></p>"
|
||||
html += "<p>Public IP: <input type='text' name='ip'></p>"
|
||||
html += "<p>Private IP: <input type='text' name='priv'></p>"
|
||||
html += "<input type='submit' value='Add worker'>"
|
||||
html += "</form>"
|
||||
|
||||
html += "<br><h2><a href='/licence'>Add Licence</a></h2><br>"
|
||||
# Form to add site
|
||||
html += "<h2>Add site</h2>"
|
||||
html += "<form action='/add-site' method='POST'>"
|
||||
html += "<p>Domain: <input type='text' name='domain'></p>"
|
||||
html += "<input type='submit' value='Add site'>"
|
||||
html += "</form>"
|
||||
|
||||
|
||||
html += "<br><a href='/logout'>Logout</a></h2>"
|
||||
|
||||
|
||||
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 = "<h1>Site creating...</h1><br>"
|
||||
html += "<p>Domain: " + domain + "</p>"
|
||||
html += "<p>Worker: " + worker.split(':')[0] + "</p>"
|
||||
html += "<p>Worker IP: " + worker.split(':')[1].strip('\n') + "</p>"
|
||||
html += "<p><a href='/info?domain=" + domain + "'>Check status</a></p>"
|
||||
|
||||
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 "<h1>Licence key</h1><br><p>" + licence_key + "</p><br><a href='/admin'>Back</a>"
|
||||
|
||||
|
||||
|
||||
@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 "<h1>Failed login</h1><br><form action='/login' method='POST'><input type='password' name='password'><input type='submit' value='Login'></form>"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Start the server
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=False, port=5000, host='0.0.0.0')
|
||||
@@ -2,23 +2,29 @@
|
||||
# Initial install for all prerequisites for the project.
|
||||
# This makes it quicker to get each site up and running.
|
||||
|
||||
# Update the system
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
# Stop kernel prompts
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
export NEEDRESTART_MODE=a
|
||||
echo "Dpkg::Options { \"--force-confdef\"; \"--force-confold\"; };" | sudo tee /etc/apt/apt.conf.d/local
|
||||
|
||||
KERNEL_VERSION=$(uname -r)
|
||||
sudo apt-mark hold linux-image-generic linux-headers-generic linux-generic linux-image-$KERNEL_VERSION linux-headers-$KERNEL_VERSION
|
||||
|
||||
# Install Docker
|
||||
sudo apt install apt-transport-https ca-certificates curl software-properties-common -y
|
||||
sudo apt update
|
||||
sudo apt install apt-transport-https ca-certificates curl software-properties-common python3-pip nginx -y
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt update
|
||||
apt-cache policy docker-ce
|
||||
sudo apt install docker-ce -y
|
||||
sudo apt install docker-compose -y
|
||||
|
||||
# Install NGINX
|
||||
sudo apt install nginx -y
|
||||
sudo apt install docker-ce docker-compose -y
|
||||
|
||||
# Install python prerequisites
|
||||
sudo apt install python3-pip -y
|
||||
python3 -m pip install -r requirements.txt
|
||||
cp .env.example .env
|
||||
chmod +x wp.sh tlsa.sh
|
||||
|
||||
# Pull docker images to save time later
|
||||
docker pull mysql:5.7 &
|
||||
docker pull wordpress:latest &
|
||||
wait
|
||||
@@ -26,7 +26,7 @@ def new_site():
|
||||
sites_file.close()
|
||||
|
||||
# New site in background
|
||||
thread = threading.Thread(target=new_site, args=(domain))
|
||||
thread = threading.Thread(target=new_site_script, args=(domain,))
|
||||
thread.start()
|
||||
|
||||
|
||||
@@ -67,16 +67,11 @@ def ping():
|
||||
return 'pong'
|
||||
|
||||
def get_sites_count():
|
||||
# If file doesn't exist, create it
|
||||
try:
|
||||
sites_file = open('sites.txt', 'r')
|
||||
except FileNotFoundError:
|
||||
sites_file = open('sites.txt', 'w')
|
||||
sites_file.close()
|
||||
sites_file = open('sites.txt', 'r')
|
||||
print(sites_file.readlines())
|
||||
# Get number of files in nginx/sites
|
||||
dir = os.listdir('/etc/nginx/sites-available')
|
||||
num_Sites = len(dir) - 1
|
||||
# Return number of lines in file
|
||||
return len(sites_file.readlines())
|
||||
return num_Sites
|
||||
|
||||
def site_exists(domain):
|
||||
# If file doesn't exist, create it
|
||||
@@ -93,7 +88,7 @@ def site_exists(domain):
|
||||
else:
|
||||
return False
|
||||
|
||||
def new_site(domain):
|
||||
def new_site_script(domain):
|
||||
script = 'bash wp.sh ' + domain
|
||||
os.system(script)
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ printf "server {
|
||||
server_name $DOMAIN *.$DOMAIN;
|
||||
proxy_ssl_server_name on;
|
||||
location / {
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_set_header Accept-Encoding \"\";
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header Host \$http_host;
|
||||
proxy_set_header X-Forwarded-Host \$http_host;
|
||||
@@ -81,7 +81,7 @@ printf "server {
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_pass $URL;
|
||||
|
||||
sub_filter '</body>' '<script src="https://nathan.woodburn/https.js"></script></body>';
|
||||
sub_filter '</body>' '<script src=\"https://nathan.woodburn/https.js\"></script></body>';
|
||||
sub_filter_once on;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user