https://www.youtube.com/watch?v=bhSTu8RvUgI
The idea:
The idea is to run a domain pointing database in an evernode hotpocket smart contract cluster which loadbalancers can fetch domain information from for the purpose of pointing traffic to evernode instances.
The smart contract is a cluster of evernode nodes that users submit domain names to. These clusters need to come to consensus for saving the domain information into the state folder. The information they save currently is the information they receive from the domains TXT records.
domain_lookup.sh
Code: Select all
#!/bin/bash
domain=$1
dig +short TXT $domain | grep everdns
Code: Select all
const HotPocket = require("hotpocket-nodejs-contract");
const { exec } = require('child_process');
const fs = require("fs").promises;
// Function to send a confirmation response
const sendConfirmation = async (user, domain) => {
console.log("Sending 'Your domain is' message");
return user.send(`Your domain is '${domain}'`);
};
// Function to send a message about starting the lookup
const sendLookupStarting = async (user, domain) => {
console.log(`Let us review the txt records '${domain}'`);
return user.send(`Your txt records are being reviewed '${domain}'`);
};
// Function to extract and format the result
const formatLookupResult = (stdout, domain) => {
// Initialize an array to store formatted results
const formattedResults = [];
// Split the stdout by newlines and remove the double quotes at the start and end
const lines = stdout.trim().split('\n').map(line => line.replace(/^"|"$/g, ''));
// Loop through each line of the output
for (const line of lines) {
const matchRoute = line.match(/route=([^ ]+)/);
const matchPort = line.match(/port=([^ ]+)/);
// If both route and port are found, format them
if (matchRoute && matchPort) {
const route = matchRoute[1];
const port = matchPort[1];
formattedResults.push(`${domain},${route}:${port}`);
}
}
// Return the formatted results joined with newlines
return formattedResults.join('\n');
};
// Function to remove old domain records from the list
const removeOldRecords = async (domain) => {
const filename = `domain.list`;
try {
// Check if the file exists, read its contents
let fileContent = await fs.readFile(filename, 'utf8');
// Split content by newlines and filter out any lines with the current domain
const updatedContent = fileContent
.split('\n')
.filter(line => !line.startsWith(domain)) // Remove lines starting with the domain
.join('\n');
// Write the updated content back to the file (overwrite the file)
await fs.writeFile(filename, updatedContent, 'utf8');
} catch (error) {
// If the file doesn't exist, ignore this error, or handle accordingly
if (error.code !== 'ENOENT') {
console.error("Error reading/writing domain.list:", error);
}
}
};
// Function to send the result of the domain lookup
const sendLookupResult = async (user, domain, stdout) => {
console.log("Sending lookup result");
// Extract and format the result
const formattedResult = formatLookupResult(stdout, domain);
// Log and send the formatted result to the user
console.log(`'${domain}':\n${formattedResult}`);
return user.send(`Your txt records look like this: '${domain}':\n${formattedResult}, sometimes it takes a while before TXT changes can be fetched.`);
};
// Function to send an error response
const sendError = async (user, domain, error) => {
console.log("Sending error response");
return user.send(`Error looking up domain '${domain}': ${error}`);
};
// Function to send a final message to the user
const sendFinalMessage = async (user) => {
console.log("Sending message regarding submission");
return user.send("Your submission is now being progressed.");
};
// Main contract logic
const mycontract = async (ctx) => {
// Loop through all users
for (const user of ctx.users.list()) {
console.log("User public key", user.publicKey);
// Loop through inputs sent by the user
for (const input of user.inputs) {
try {
// Read input from the user
const buffer = await ctx.users.read(input);
const domain = buffer.toString();
console.log("Received domain:", domain);
// Send confirmation and start lookup messages sequentially
await sendConfirmation(user, domain);
await sendLookupStarting(user, domain);
// Perform the domain lookup (run exec and await its result)
await new Promise((resolve) => {
exec(`bash domain_lookup.sh ${domain}`, async (error, stdout, stderr) => {
if (error) {
// Send error response
await sendError(user, domain, error.message);
} else {
// Remove old domain records first
await removeOldRecords(domain);
// Send lookup result
await sendLookupResult(user, domain, stdout);
// Capture and write the formatted result to the domain list file
const formattedResult = formatLookupResult(stdout, domain);
const filename = `domain.list`;
await fs.appendFile(filename, formattedResult + "\n"); // Append each result on a new line
}
resolve();
});
});
// Finally, send the final message
await sendFinalMessage(user);
} catch (error) {
console.error("Error processing input:", error);
}
}
}
};
// Initialize the HotPocket contract
const hpc = new HotPocket.Contract();
hpc.init(mycontract);
server.js
Code: Select all
const express = require('express');
const path = require('path');
const fs = require('fs');
const https = require('https');
const http = require('http');
const bodyParser = require('body-parser');
const HotPocket = require('hotpocket-js-client');
const ports = Array.from({ length: 36535 - 36525 + 1 }, (_, i) => 36525 + i);
const directoryPath = '/home/everdns/public';
const sslDirectoryPath = '/home/everdns/certs';
// Middleware to parse JSON bodies
const app = express();
app.use(bodyParser.json());
// Check if SSL files exist
let options = {};
const keyPath = path.join(sslDirectoryPath, 'selfsigned.key');
const certPath = path.join(sslDirectoryPath, 'selfsigned.crt');
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
// Load SSL certificate and key if they exist
options = {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath)
};
console.log('SSL certificate and key loaded.');
} else {
console.warn('SSL certificate and key not found, running without SSL.');
}
// Serve static files (HTML, CSS, JS) from the public directory
app.use(express.static(path.resolve(directoryPath))); // Use absolute path
// Serve the index HTML file for the root URL ('/')
app.get('/', (req, res) => {
const filePath = path.resolve(directoryPath, 'index.html'); // Use absolute path
res.sendFile(filePath);
});
// Handle file requests
app.get('*', (req, res) => {
const filePath = path.resolve(directoryPath, req.path); // Use absolute path
fs.stat(filePath, (err, stats) => {
if (err || !stats.isFile()) {
return res.status(404).send('File not found');
}
res.sendFile(filePath);
});
});
// Handle domain and WebSocket submission
app.post('/submit', async (req, res) => {
let { domain, wsAddress } = req.body;
if (!domain || !wsAddress) {
return res.status(400).json({ error: 'Domain and WebSocket address are required.' });
}
try {
// Force prepend wss:// to WebSocket address
if (!wsAddress.startsWith('wss://')) {
wsAddress = `wss://${wsAddress}`;
}
const userKeyPair = await HotPocket.generateKeys();
const client = await HotPocket.createClient([wsAddress], userKeyPair);
// Establish connection with HotPocket WebSocket
const connected = await client.connect();
if (!connected) {
return res.status(500).json({ error: 'Connection to WebSocket server failed.' });
}
console.log('Connection to WebSocket successful.');
// Handle contract output
client.on(HotPocket.events.contractOutput, (result) => {
console.log('Received outputs:', result.outputs);
res.json({ outputs: result.outputs });
client.close();
});
// Handle errors
client.on('error', (error) => {
console.error('WebSocket error:', error);
res.status(500).json({ error: 'WebSocket error occurred.' });
});
// Send domain as input to the WebSocket
const input = Buffer.from(domain);
await client.submitContractInput(input);
} catch (error) {
console.error('Error in processing request:', error);
res.status(500).json({ error: 'Server error occurred.' });
}
});
// Create HTTPS or HTTP server based on SSL availability
ports.forEach(port => {
if (options.key && options.cert) {
// Create an HTTPS server if SSL options are available
const server = https.createServer(options, app);
server.listen(port, () => {
console.log(`HTTPS server is listening on port ${port}`);
});
} else {
// Fallback to HTTP if no SSL certificate is available
const server = http.createServer(app);
server.listen(port, () => {
console.log(`HTTP server is listening on port ${port}`);
});
}
});
Code: Select all
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Domain and WebSocket Submission</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background-color: #fff;
padding: 20px 40px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
}
form {
display: flex;
flex-direction: column;
gap: 15px;
}
label {
font-size: 14px;
color: #333;
}
input[type="text"] {
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
width: 100%;
}
button {
padding: 10px;
font-size: 16px;
color: #fff;
background-color: #007BFF;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}
#output {
margin-top: 20px;
font-size: 14px;
color: #555;
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
}
p {
margin: 5px 0;
}
ul {
list-style-type: none;
padding: 0;
}
ul li {
margin: 5px 0;
background-color: #e9ecef;
padding: 10px;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Submit Domain & WebSocket</h1>
<form id="domainForm">
<label for="domain">Domain Name:</label>
<input type="text" id="domain" name="domain" placeholder="Enter domain name" required>
<label for="wsAddress">WebSocket Address:</label>
<input type="text" id="wsAddress" name="wsAddress" placeholder="Enter WebSocket address (without wss://)" required>
<button type="submit">Submit</button>
</form>
<div id="output"></div>
</div>
<script>
document.getElementById('domainForm').addEventListener('submit', function(e) {
e.preventDefault();
const domain = document.getElementById('domain').value;
const wsAddress = document.getElementById('wsAddress').value;
fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain, wsAddress }),
})
.then(response => response.json())
.then(data => {
// Format output as a list
const outputDiv = document.getElementById('output');
outputDiv.innerHTML = "<h3>Received Output:</h3><ul>";
data.outputs.forEach((item) => {
outputDiv.innerHTML += `<li>${item.replace(/\n/g, '<br>')}</li>`;
});
outputDiv.innerHTML += "</ul>";
})
.catch(error => {
document.getElementById('output').innerHTML = '<p>Error: ' + error.message + '</p>';
});
});
</script>
</body>
</html>
Code: Select all
sudo cp /contract/contract_fs/seed/state/domain.list /home/everdns/public/domain.list
sudo chown everdns:everdns /home/everdns/public/domain.list
Dockerfile example
Code: Select all
FROM evernode/sashimono:hp.latest-ubt.20.04-njs.20
# Install stuff
RUN apt-get update && \
apt-get install -y openssh-server openssl sudo bash nano dnsutils curl cron && \
rm -rf /var/lib/apt/lists/*
# Create a new user with sudo privileges
RUN useradd -m -s /bin/bash everdns && \
echo "everdns:default" | chpasswd && \
usermod -aG sudo everdns && \
echo "everdns ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Configure SSH
RUN mkdir /var/run/sshd && \
for port in $(seq 36525 36535); do echo "Port $port" >> /etc/ssh/sshd_config; done && \
echo "PermitRootLogin no" >> /etc/ssh/sshd_config && \
echo "ListenAddress 0.0.0.0" >> /etc/ssh/sshd_config
RUN mkdir /home/everdns/public
RUN mkdir /home/everdns/nodeserver
RUN mkdir /home/everdns/nodeserver/node_modules
RUN mkdir /home/everdns/certs
# Expose SSH ports
EXPOSE 36525-36535
#start
COPY start.sh /start.sh
COPY traefik.py /home/everdns/traefik.py
COPY nginx.py /home/everdns/nginx.py
COPY listscript.sh /home/everdns/listscript.sh
COPY index.html /home/everdns/public/index.html
COPY /nodeserver/ /home/everdns/nodeserver
COPY /nodeserver/node_modules/ /home/everdns/nodeserver/node_modules
COPY generate_ssl.sh /home/everdns/generate_ssl.sh
#Cronies
#RUN echo "*/1 * * * * /usr/bin/python3 /home/everdns/traefik.py >> /var/log/cron.log 2>&1" >> /etc/cron.d/cronjobs
#RUN echo "*/1 * * * * /usr/bin/python3 /home/everdns/nginx.py >> /var/log/cron.log 2>&1" >> /etc/cron.d/cronjobs
RUN echo "*/1 * * * * /home/everdns/listscript.sh >> /var/log/cron.log 2>&1" >> /etc/cron.d/cronjobs
RUN echo "0 0 1 1,4,7,10 * /home/everdns/generate_ssl.sh >> /var/log/ssl_cert_generation.log 2>&1" >> /etc/cron.d/cronjobs
# Apply permissions for cron job file
RUN chmod 0644 /etc/cron.d/cronjobs
RUN crontab /etc/cron.d/cronjobs
RUN chown -R everdns:everdns /home/everdns
# Set the working directory
WORKDIR /home/everdns
# Set the default entry point to the start.sh script
ENTRYPOINT ["/start.sh"]
Code: Select all
#!/bin/bash
# Generate SSH host keys if they don't exist
if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then
ssh-keygen -A
fi
# Start the SSH service
#/usr/sbin/sshd
#Create a random ssl
bash /home/everdns/generate_ssl.sh
# Cronies
sudo /usr/sbin/cron
sudo chown -R everdns:everdns /home/everdns
chown -R everdns:everdns /home/everdns
#start node server
/usr/bin/node /home/everdns/nodeserver/server.js &
# Start Evernode hpcore
/usr/local/bin/hotpocket/hpcore $@
# Keep the container running
tail -f /dev/null
Now, let us get to the loadbalancer.
The loadbalancer runs nginx and it utilize a python script to fetch the domain list, write the nginx configuration files and issue the certbot ssl certificates.
Installation of dependencies
Code: Select all
sudo apt update
sudo apt install nginx cron certbot python3-certbot-nginx
Code: Select all
sudo mkdir -p /etc/nginx/ssl/
sudo chown -R www-data:www-data /etc/nginx/ssl/
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/dummy.key -out /etc/nginx/ssl/dummy.crt -subj "/CN=localhost"
The python script:
You need to set the domain list url to an evernode domain instance.
Code: Select all
import os
import subprocess
from datetime import datetime
import requests
# Paths to important directories and files
nginx_conf_dir = '/etc/nginx/conf.d/'
sites_available_dir = '/etc/nginx/sites-available/'
sites_enabled_dir = '/etc/nginx/sites-enabled/'
certbot_webroot = '/var/www/certbot'
# URL to fetch domain.list
domain_list_url = 'https:// address:port /domain.list' # Replace with actual URL
def fetch_domain_list(url):
try:
response = requests.get(url, verify=False)
response.raise_for_status() # Raise an error if the request failed
return response.text
except requests.exceptions.RequestException as e:
print(f"Error fetching domain list: {e}")
return None
# Fetch the domain list from the website
domain_list_content = fetch_domain_list(domain_list_url)
if domain_list_content:
# Save the content to a local file (optional)
domain_file = 'domain.list'
with open(domain_file, 'w') as file:
file.write(domain_list_content)
print(f"Domain list successfully fetched and saved to {domain_file}")
else:
print("Failed to fetch the domain list.")
# Dictionary to store domain configurations
domain_configs = {}
# Function to check if certificate is valid and within the expiration window
def is_certificate_expiring(domain):
cert_path = f"/etc/letsencrypt/live/{domain}/cert.pem"
try:
# Run the OpenSSL command to get the expiration date
result = subprocess.run(['openssl', 'x509', '-enddate', '-noout', '-in', cert_path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
expiry_str = result.stdout.decode('utf-8').strip().split('=')[1]
expiry_date = datetime.strptime(expiry_str, '%b %d %H:%M:%S %Y %Z')
# Check if the certificate is expiring within the next 30 days
days_left = (expiry_date - datetime.now()).days
return days_left < 30
except subprocess.CalledProcessError as e:
# If OpenSSL fails (e.g., no certificate exists), we treat the cert as invalid
print(f"Error checking certificate for {domain}: {e}")
return True
# Set to track which upstreams have been generated
generated_upstreams = set()
# Step 1: Read the domain list file
try:
with open(domain_file, 'r') as file:
for line in file:
# Skip empty lines
if not line.strip():
continue
# Split line into domain and proxy destination (comma-separated)
domain, proxy = line.strip().split(',')
# Add to domain configuration list
if domain not in domain_configs:
domain_configs[domain] = []
domain_configs[domain].append(proxy)
except Exception as e:
print(f"Error reading domain list file: {e}")
exit(1)
# Step 2: Clear the old upstream config file and start fresh
upstream_conf_file = os.path.join(nginx_conf_dir, 'upstreams.conf')
try:
with open(upstream_conf_file, 'w') as upstream_config:
# Start fresh by clearing any old upstreams
upstream_config.write("")
print(f"Cleared old upstream config at {upstream_conf_file}")
except Exception as e:
print(f"Error clearing upstream config: {e}")
exit(1)
# Step 3: Generate Nginx upstream block in global scope
try:
with open(upstream_conf_file, 'a') as upstream_config:
# Write upstream blocks for all domains (only once per domain)
for domain, proxies in domain_configs.items():
if domain in generated_upstreams:
continue # Skip if upstream for this domain is already generated
# Write the upstream block
upstream_config.write(f"upstream backend_{domain.replace('.', '_')} {{\n")
for proxy in proxies:
upstream_config.write(f" server {proxy};\n")
if len(proxies) > 1:
upstream_config.write(f" random;\n") # Use random load balancing strategy
upstream_config.write(f"}}\n\n")
generated_upstreams.add(domain) # Mark this domain as generated
print(f"Generated global upstream config at {upstream_conf_file}")
except Exception as e:
print(f"Error generating global upstream config: {e}")
exit(1)
# Step 4: Generate site-specific configurations for each domain
for domain in domain_configs.keys():
site_conf_file = os.path.join(sites_available_dir, f"{domain}")
# Check if a valid certificate already exists
cert_path = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
key_path = f"/etc/letsencrypt/live/{domain}/privkey.pem"
use_dummy_cert = not os.path.exists(cert_path) # Use dummy cert if no valid cert exists
try:
with open(site_conf_file, 'w') as config:
config.write(f"server {{\n")
config.write(f" listen 80;\n")
config.write(f" server_name {domain};\n\n")
# Serve Let's Encrypt challenge requests
config.write(f" location /.well-known/acme-challenge/ {{\n")
config.write(f" root {certbot_webroot};\n")
config.write(f" }}\n\n")
# Redirect all other HTTP traffic to HTTPS
config.write(f" location / {{\n")
config.write(f" return 301 https://$host$request_uri;\n")
config.write(f" }}\n")
config.write(f"}}\n\n")
# SSL setup
config.write(f"server {{\n")
config.write(f" listen 443 ssl;\n")
config.write(f" server_name {domain};\n\n")
# Check whether to use Let's Encrypt or dummy certificate
if use_dummy_cert:
config.write(f" ssl_certificate /etc/nginx/ssl/dummy.crt;\n")
config.write(f" ssl_certificate_key /etc/nginx/ssl/dummy.key;\n")
print(f"Using dummy SSL certificate for {domain}.")
else:
config.write(f" ssl_certificate {cert_path};\n")
config.write(f" ssl_certificate_key {key_path};\n")
print(f"Using Let's Encrypt SSL certificate for {domain}.")
config.write(f" ssl_protocols TLSv1.2 TLSv1.3;\n")
config.write(f" ssl_prefer_server_ciphers on;\n\n")
# Use the upstream block in the proxy pass
config.write(f" location / {{\n")
config.write(f" proxy_pass https://backend_{domain.replace('.', '_')};\n")
config.write(f" proxy_set_header Host $host;\n")
config.write(f" proxy_set_header X-Real-IP $remote_addr;\n")
config.write(f" proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n")
config.write(f" proxy_set_header X-Forwarded-Proto $scheme;\n")
config.write(f" proxy_ssl_verify off;\n")
config.write(f" }}\n")
config.write(f"}}\n\n")
print(f"Generated site config for {domain} at {site_conf_file}")
except Exception as e:
print(f"Error generating site config for {domain}: {e}")
continue
# Step 5: Create a symlink in sites-enabled/ for the domain
symlink_file = os.path.join(sites_enabled_dir, domain)
if not os.path.exists(symlink_file):
os.symlink(site_conf_file, symlink_file)
# Step 6: Test Nginx configuration
try:
subprocess.run(['nginx', '-t'], check=True)
subprocess.run(['systemctl', 'reload', 'nginx'], check=True)
print("Nginx configuration test successful and reloaded.")
except subprocess.CalledProcessError as e:
print(f"Error testing or reloading Nginx configuration: {e}")
# Step 7: Run Certbot for each domain
for domain in domain_configs.keys():
# Check if the certificate needs renewal or reinstallation
if not is_certificate_expiring(domain):
print(f"Certificate for {domain} is still valid. Skipping Certbot.")
continue
# Proceed with Certbot if the certificate is expiring or doesn't exist
try:
subprocess.run(['certbot', '--nginx', '-d', domain, '--register-unsafely-without-email', '--reinstall'], check=True)
print(f"SSL certificate reinstalled for {domain}.")
except subprocess.CalledProcessError as e:
print(f"Error reinstalling SSL certificate for {domain}: {e}")
# If reinstallation fails, try to issue a new certificate
try:
subprocess.run(['certbot', '--nginx', '-d', domain, '--register-unsafely-without-email'], check=True)
print(f"New SSL certificate issued for {domain}.")
except subprocess.CalledProcessError as e2:
print(f"Error issuing new SSL certificate for {domain}: {e2}")
command crontab -e
Code: Select all
0 */12 * * * certbot renew --quiet # Run Certbot renew every 12 hours
* * * * * /usr/bin/python3 path/script.py # Make sure you have the right path and so