diff --git a/README.md b/README.md index 9dffc10..0982b78 100644 --- a/README.md +++ b/README.md @@ -1 +1,29 @@ -# hnshosting-bot +# HNS Hosting Discord Bot + +## Setup +Install the python requirements with `pip install -r requirements.txt` +Add your discord token to a .env file in the root directory of the project +```sh +DISCORD_TOKEN=your_token_here +``` + +Install nginx +```sh +sudo apt update +sudo apt install nginx -y +``` + +## Run + +You can run the bot with +```sh +python3 main.py +``` + +To run the bot in the background, you can use `screen` +```sh +screen -S bot +python3 main.py +``` +Close the screen with `Ctrl + A + D` (keeps the process running) +Resume the screen with `screen -r bot` \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..4f956e6 --- /dev/null +++ b/bot.py @@ -0,0 +1,167 @@ +import subprocess +import os +from dotenv import load_dotenv +import discord +from discord import app_commands + +load_dotenv() +TOKEN = os.getenv('DISCORD_TOKEN') + +intents = discord.Intents.default() +client = discord.Client(intents=intents) +tree = app_commands.CommandTree(client) + +@tree.command(name = "mirror", description = "Create a mirror of an ICANN site on a Handshake domain") +async def mirror(interaction, handshakedomain: str, icannurl: str): + print("Creating mirror to " + icannurl + " from " + handshakedomain + "...") + if not icannurl.startswith("https://"): + await interaction.response.send_message("Please use https:// for the ICANN URL", ephemeral=True) + return + + await interaction.response.send_message("Creating mirror to " + icannurl + " from " + handshakedomain + "..." + "\nCheck your DM for the status of your mirror.", ephemeral=True) + # Get user from interaction + user = interaction.user + handshakedomain_str = str(handshakedomain) + icannurl_str = str(icannurl) + user_str = str(user.id) + command = ['./proxy.sh', handshakedomain_str, icannurl_str, user_str] + try: + output = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True) + print("TLSA output:") + tlsa = get_tlsa(output) + if tlsa is not None: + print(tlsa) + message = "Mirror setup! Add this TLSA record\n`_443._tcp." + handshakedomain_str + "` : `" + tlsa + "`\nAdd this A\n`" + handshakedomain_str + "` : `152.69.188.246`" + await user.send(message) + else: + error = get_error(output) + await user.send(error) + + except subprocess.CalledProcessError as e: + print(f"Command execution failed with exit code {e.returncode}.") + await user.send(f"Command execution failed.") + updateStatus() + +@tree.command(name = "delete", description = "Delete a Handshake domain from the system") +async def delete(interaction, handshakedomain: str): + print("Deleting " + handshakedomain + "...") + await interaction.response.send_message("Deleting " + handshakedomain + "..." + "\nCheck your DM for status.", ephemeral=True) + # Get user from interaction + user = interaction.user + handshakedomain_str = str(handshakedomain) + user_str = str(user.id) + + # Construct the command + command = ['./delete.sh', handshakedomain_str, user_str] + try: + output = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True) + print("Delete output:") + out=output.split("\n")[0] + print(out) + await user.send(out) + except subprocess.CalledProcessError as e: + print(f"Command execution failed with exit code {e.returncode}.") + await user.send(f"Command execution failed.") + updateStatus() + +@tree.command(name = "list", description = "List all Handshake mirrors") +async def list(interaction): + print("Listing mirrors...") + # Get user from interaction + user = interaction.user + if user.id != 892672018917519370: + await interaction.response.send_message("You don't have permission to do that.", ephemeral=True) + print(user + " tried to list mirrors.") + return + + # List all files in the directory using os + files = os.listdir("/etc/nginx/sites-available") + # Remove default + files.remove("default") + await interaction.response.send_message("Here are all the mirrors:\n" + "\n".join(files)) + +@tree.command(name = "tlsa", description = "Get the TLSA record for an existing Handshake domain") +async def tlsa(interaction, handshakedomain: str): + print("Getting TLSA record for " + handshakedomain + "...") + # Get user from interaction + output = subprocess.check_output(['./tlsa.sh', handshakedomain], stderr=subprocess.STDOUT, text=True) + await interaction.response.send_message(output, ephemeral=True) + +@tree.command(name = "git", description = "Create a website from a git repo of html files") +async def git(interaction, handshakedomain: str, giturl: str): + print("Creating website from " + giturl + " on " + handshakedomain + "...") + if not giturl.startswith("https://"): + await interaction.response.send_message("Please use https:// for the git URL", ephemeral=True) + return + await interaction.response.send_message("Creating website from " + giturl + " on " + handshakedomain + "..." + "\nCheck your DM for the status of your website.", ephemeral=True) + user = interaction.user + handshakedomain_str = str(handshakedomain) + user_str = str(user.id) + giturl_str = str(giturl) + output = subprocess.check_output(['./git.sh', handshakedomain_str, giturl_str, user_str], stderr=subprocess.STDOUT, text=True) + # Check if output contains any errors + lowercase_string = output.lower() + # Check if the lowercase string contains the word "error" + if "error" in lowercase_string: + await user.send("Failed with error:\n" + output) + return + + output = subprocess.check_output(['./tlsa.sh', handshakedomain_str], stderr=subprocess.STDOUT, text=True) + # Get only second line + output = output.split("\n")[1] + await user.send("Website created! Add this TLSA record\n`_443._tcp." + handshakedomain_str + "` : `" + output + "`\nAdd this A\n`" + handshakedomain_str + "` : `152.69.188.246`") + +@tree.command(name = "gitpull", description = "Get the latest changes from a git repo of html files") +async def gitpull(interaction, handshakedomain: str): + print("Pulling latest changes from " + handshakedomain + "...") + await interaction.response.send_message("Pulling changes for " + handshakedomain + "...", ephemeral=True) + user = interaction.user + handshakedomain_str = str(handshakedomain) + user_str = str(user.id) + output = subprocess.check_output(['./gitpull.sh', handshakedomain_str, user_str], stderr=subprocess.STDOUT, text=True) + # Check if output contains any errors + lowercase_string = output.lower() + # Check if the lowercase string contains the word "error" + if "error" in lowercase_string: + await user.send("Failed with error:\n" + output) + return + + await user.send("Changes pulled for " + handshakedomain + "!") + + + + + + + +def get_tlsa(input_string): + lines = input_string.split("\n") + for line in lines: + if line.strip().startswith("TLSA:"): + return line[6:] + return None + +def get_error(input_string): + lines = input_string.split("\n") + for line in lines: + if line.strip().startswith("ERROR:"): + return line + return None + +def updateStatus(): + # List all files in the directory using os + files = os.listdir("/etc/nginx/sites-available") + # Count the number of files - 1 (default) + count = len(files) - 1 + # Set the status + activity = discord.Activity(name=str(count) + " mirrors", type=discord.ActivityType.watching) + # Update the status + client.loop.create_task(client.change_presence(activity=activity)) + +@client.event +async def on_ready(): + await tree.sync() + print("Ready!") + updateStatus() + +client.run(TOKEN) \ No newline at end of file diff --git a/delete.sh b/delete.sh new file mode 100644 index 0000000..7fcd425 --- /dev/null +++ b/delete.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +domain=$1 +user=$2 + +# Set all to lowercase +domain=${domain,,} +user=${user,,} + +file_path="/etc/nginx/sites-available/$domain" + +# Check if domain already exists +if [ -f $file_path ]; then + # Verify owner + if grep -q "$user" "$file_path"; then + rm $file_path + rm /etc/nginx/sites-enabled/$domain + systemctl restart nginx + echo "Domain deleted!" + else + echo "ERROR: You do not own this domain" + exit 0; + fi +else + echo "ERROR: Domain doesn't exists" + exit 0; +fi + +# Check if website files exist +if [ -d "/var/www/$domain" ]; then + rm -rf /var/www/$domain + echo "Website files deleted!" +fi \ No newline at end of file diff --git a/git.sh b/git.sh new file mode 100644 index 0000000..39828c8 --- /dev/null +++ b/git.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# This script is used to setup nginx for a static website using files from a git repository. +# Make sure the git repo has an `index.html` and `404.html` file. + +# Usage ./git.sh [domain] [git repo url] [optional: user] +# Example ./git.sh nathan.woodburn https://github.com/Nathanwoodburn/Nathanwoodburn.github.io.git + +# Variables +domain=$1 +git_repo=$2 +user=$3 + +# Check if domain name is set +if [ -z "$1" ] +then + echo "Domain name:" + read domain +fi + +# Check if git repo is set +if [ -z "$2" ] +then + echo "Git repo:" + read git_repo +fi + +# Check if nginx is installed +if ! [ -x "$(command -v nginx)" ]; then + sudo apt update + sudo apt install nginx -y +fi + +# Clone git repo +git clone $git_repo /var/www/$domain + + +# Setup NGINX config +printf "# $user +server { + listen 80; + listen [::]:80; + root /var/www/$domain; + index index.html; + server_name $domain *.$domain; + + location / { + try_files \$uri \$uri/ @htmlext; + } + + location ~ \.html$ { + try_files \$uri =404; + } + + location @htmlext { + rewrite ^(.*)$ \$1.html last; + } + error_page 404 /404.html; + location = /404.html { + internal; + } + location = /.well-known/wallets/HNS { + add_header Cache-Control 'must-revalidate'; + add_header Content-Type text/plain; + } + listen 443 ssl; + ssl_certificate /etc/ssl/$domain.crt; + ssl_certificate_key /etc/ssl/$domain.key; +} +" > /etc/nginx/sites-available/$domain +sudo ln -s /etc/nginx/sites-available/$domain /etc/nginx/sites-enabled/$domain + +#generate ssl certificate +openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ + -keyout cert.key -out cert.crt -extensions ext -config \ + <(echo "[req]"; + echo distinguished_name=req; + echo "[ext]"; + echo "keyUsage=critical,digitalSignature,keyEncipherment"; + echo "extendedKeyUsage=serverAuth"; + echo "basicConstraints=critical,CA:FALSE"; + echo "subjectAltName=DNS:$domain,DNS:*.$domain"; + ) -subj "/CN=*.$domain" + +TLSA=$(echo -n "3 1 1 " && openssl x509 -in cert.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | xxd -p -u -c 32) + +echo "TLSA: $TLSA" + +sudo mv cert.key /etc/ssl/$domain.key +sudo mv cert.crt /etc/ssl/$domain.crt + +# Restart to apply config file +sudo systemctl restart nginx \ No newline at end of file diff --git a/gitpull.sh b/gitpull.sh new file mode 100644 index 0000000..58d98f3 --- /dev/null +++ b/gitpull.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Variables +domain=$1 +user=$2 +# Verify owner +file_path="/etc/nginx/sites-available/$domain" +if grep -q "$user" "$file_path"; then + git -C /var/www/$domain pull + echo "Git pull complete!" +else + echo "ERROR: You do not own this domain" +fi + diff --git a/proxy.sh b/proxy.sh new file mode 100644 index 0000000..b55dc59 --- /dev/null +++ b/proxy.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +domain=$1 +url=$2 +user=$3 + +# Set all to lowercase +domain=${domain,,} +url=${url,,} +user=${user,,} + +# Check if domain already exists +if [ -f /etc/nginx/sites-available/$domain ]; then + echo "ERROR: Domain already exists" + exit 0; +fi + + +# Verify url is valid timeout 5 seconds + +valid=$(timeout 5 curl -s -o /dev/null -w "%{http_code}" $url | grep 200) +if [ -n "$valid" ]; then + echo "URL is valid: $url" +else + echo "ERROR: URL is not valid or unreachable: $url" + exit 0; +fi + + +printf "# $user +server { + listen 80; + listen [::]:80; + server_name $domain; + proxy_ssl_server_name on; + location / { + proxy_set_header X-Real-IP \$remote_addr; + proxy_pass $url; + } + + listen 443 ssl; + ssl_certificate /etc/ssl/$domain.crt; + ssl_certificate_key /etc/ssl/$domain.key; +}" > /etc/nginx/sites-available/$domain +sudo ln -s /etc/nginx/sites-available/$domain /etc/nginx/sites-enabled/$domain + +#generate ssl certificate +openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \ + -keyout cert.key -out cert.crt -extensions ext -config \ + <(echo "[req]"; + echo distinguished_name=req; + echo "[ext]"; + echo "keyUsage=critical,digitalSignature,keyEncipherment"; + echo "extendedKeyUsage=serverAuth"; + echo "basicConstraints=critical,CA:FALSE"; + echo "subjectAltName=DNS:$domain,DNS:*.$domain"; + ) -subj "/CN=*.$domain" + +# Respond with TLSA + +TLSA=$(echo -n "3 1 1 " && openssl x509 -in cert.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | xxd -p -u -c 32) + +echo "TLSA: $TLSA" + +sudo mv cert.key /etc/ssl/$domain.key +sudo mv cert.crt /etc/ssl/$domain.crt + +# Restart to apply config file +sudo systemctl restart nginx \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bb48990 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord +discord.py +python-dotenv \ No newline at end of file diff --git a/tlsa.sh b/tlsa.sh new file mode 100644 index 0000000..ccb71c1 --- /dev/null +++ b/tlsa.sh @@ -0,0 +1,12 @@ +#!/bin/bash +domain=$1 +# Check if args passed +if [ -z "$1" ] +then +# Ask for domain name + echo "Domain name:" + read domain +fi + +echo "TLSA record:" +echo -n "3 1 1 " && openssl x509 -in /etc/ssl/$domain.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | xxd -p -u -c 32 \ No newline at end of file