Warrant canary, better registration service management

This commit is contained in:
Sangye Ince-Johannsen 2025-04-04 00:16:53 +00:00
parent 90242a495e
commit 4ce1ff8cfd
7 changed files with 265 additions and 323 deletions

1
.gitignore vendored
View file

@ -35,6 +35,7 @@ registrations.json
banned_ips.txt
refresh_token.sh
testbench/
registration.pid
# Backup directories
backup/

286
canary.py Normal file → Executable file
View file

@ -5,58 +5,31 @@ import requests
import feedparser
import datetime
import subprocess
import json
import os
import sys
import asyncio
from time import sleep
from pathlib import Path
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# File paths
CONFIG_FILE = "config.yaml"
OUTPUT_FILE = "canary.txt"
TEMP_MESSAGE_FILE = "temp_canary_message.txt"
# Possible attestations
ATTESTATIONS = [
"We have not received any National Security Letters.",
"We have not received any court orders under the Foreign Intelligence Surveillance Act.",
"We have not received any gag orders that prevent us from stating we have received legal process.",
"We have not been required to modify our systems to facilitate surveillance.",
"We have not been subject to any searches or seizures of our servers."
]
def load_config():
"""Load configuration from YAML file."""
try:
if not os.path.exists(CONFIG_FILE):
print(f"Error: Configuration file '{CONFIG_FILE}' not found.")
print("Please create a configuration file with the following structure:")
print("""
gpg:
key_id: YOUR_GPG_KEY_ID
matrix:
enabled: true
homeserver: https://we2.ee
username: @canary:we2.ee
password: YOUR_PASSWORD
room_id: !l7XTTF6tudReoEJEvr:we2.ee
""")
sys.exit(1)
with open(CONFIG_FILE, 'r') as file:
config = yaml.safe_load(file)
# Check for required fields
required_fields = [
('gpg', 'key_id')
]
for section, field in required_fields:
required = [('gpg', 'key_id'), ('canary', 'organization'), ('canary', 'attestations')]
for section, field in required:
if section not in config or field not in config[section]:
print(f"Error: Required configuration field '{section}.{field}' is missing.")
print(f"Error: Missing required field '{section}.{field}' in config.")
sys.exit(1)
return config
except Exception as e:
print(f"Error loading configuration: {e}")
@ -67,30 +40,42 @@ def get_current_date():
return datetime.datetime.now().strftime("%Y-%m-%d")
def get_nist_time():
"""Get the current time from NIST time server."""
try:
response = requests.get("https://timeapi.io/api/Time/current/zone?timeZone=UTC", timeout=10)
if response.status_code == 200:
time_data = response.json()
return f"{time_data['dateTime']} UTC"
else:
print(f"Error fetching NIST time: HTTP {response.status_code}")
return None
except Exception as e:
print(f"Error fetching NIST time: {e}")
return None
"""Get the current time from NIST or fallback servers."""
session = requests.Session()
retry_strategy = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
endpoints = [
"https://timeapi.io/api/Time/current/zone?timeZone=UTC",
"https://worldtimeapi.org/api/timezone/UTC",
]
for url in endpoints:
try:
response = session.get(url, timeout=10)
response.raise_for_status()
data = response.json()
if "dateTime" in data:
return data["dateTime"] + " UTC"
elif "utc_datetime" in data:
return data["utc_datetime"] + " UTC"
print(f"Warning: Unexpected response format from {url}")
except requests.exceptions.RequestException as e:
print(f"Error fetching NIST time from {url}: {e}")
return None
def get_democracy_now_headline():
"""Get the latest headline from Democracy Now! RSS feed."""
def get_rss_headline(config):
"""Get the latest headline and link from the configured RSS feed."""
try:
feed = feedparser.parse("https://www.democracynow.org/democracynow.rss")
rss_config = config.get('rss', {})
rss_url = rss_config.get('url', 'https://www.democracynow.org/democracynow.rss')
feed = feedparser.parse(rss_url)
if feed.entries and len(feed.entries) > 0:
return feed.entries[0].title
else:
print("No entries found in Democracy Now! RSS feed")
return None
entry = feed.entries[0]
return {"title": entry.title, "link": entry.link}
print(f"No entries found in RSS feed: {rss_url}")
return None
except Exception as e:
print(f"Error fetching Democracy Now! headline: {e}")
print(f"Error fetching RSS headline: {e}")
return None
def get_bitcoin_latest_block():
@ -99,13 +84,13 @@ def get_bitcoin_latest_block():
response = requests.get("https://blockchain.info/latestblock", timeout=10)
if response.status_code == 200:
data = response.json()
# Get block details
block_response = requests.get(f"https://blockchain.info/rawblock/{data['hash']}", timeout=10)
if block_response.status_code == 200:
block_data = block_response.json()
hash_str = data["hash"].lstrip("0") or "0"
return {
"height": data["height"],
"hash": data["hash"],
"hash": hash_str,
"time": datetime.datetime.fromtimestamp(block_data["time"]).strftime("%Y-%m-%d %H:%M:%S UTC")
}
print(f"Error fetching Bitcoin block: HTTP {response.status_code}")
@ -114,87 +99,88 @@ def get_bitcoin_latest_block():
print(f"Error fetching Bitcoin block data: {e}")
return None
def collect_attestations():
"""Prompt user for each attestation."""
def collect_attestations(config):
"""Prompt user for each attestation from config."""
selected_attestations = []
org = config['canary']['organization']
print("\nPlease confirm each attestation separately:")
for i, attestation in enumerate(ATTESTATIONS, 1):
for i, attestation in enumerate(config['canary']['attestations'], 1):
while True:
response = input(f"Confirm attestation {i}: '{attestation}' (y/n): ").lower()
response = input(f"Confirm: '{org} {attestation}' (y/n): ").lower()
if response in ['y', 'n']:
break
print("Please answer 'y' or 'n'.")
if response == 'y':
selected_attestations.append(attestation)
return selected_attestations
def get_optional_note():
"""Prompt user for an optional note."""
note = input("\nAdd an optional note (press Enter to skip): ").strip()
return note if note else None
def create_warrant_canary_message(config):
"""Create the warrant canary message with attestations and verification elements."""
"""Create the warrant canary message with updated formatting."""
current_date = get_current_date()
nist_time = get_nist_time()
democracy_now_headline = get_democracy_now_headline()
rss_data = get_rss_headline(config)
bitcoin_block = get_bitcoin_latest_block()
# Check if all required elements are available
if not all([nist_time, democracy_now_headline, bitcoin_block]):
if not all([nist_time, rss_data, bitcoin_block]):
missing = []
if not nist_time: missing.append("NIST time")
if not democracy_now_headline: missing.append("Democracy Now! headline")
if not rss_data: missing.append(f"{config['rss'].get('name', 'RSS')} headline")
if not bitcoin_block: missing.append("Bitcoin block data")
print(f"Error: Could not fetch: {', '.join(missing)}")
return None
# Collect attestations from user
attestations = collect_attestations()
attestations = collect_attestations(config)
if not attestations:
print("Warning: No attestations were confirmed.")
proceed = input("Do you want to proceed without any attestations? (y/n): ").lower()
proceed = input("No attestations confirmed. Proceed anyway? (y/n): ").lower()
if proceed != 'y':
print("Operation cancelled")
return None
# Create the message
message = f"""We2.ee Warrant Canary
Date: {current_date}
"""
note = get_optional_note()
org = config['canary']['organization']
admin_name = config['canary'].get('admin_name', 'Admin')
admin_title = config['canary'].get('admin_title', 'administrator')
rss_name = config['rss'].get('name', 'RSS Feed')
# Add attestations
# No leading \n; GPG adds one blank line after Hash: SHA512
message = f"{org} Warrant Canary · {nist_time}\n"
message += f"I, {admin_name}, the {admin_title} of {org}, state this {datetime.datetime.now().strftime('%dth day of %B, %Y')}:\n"
for i, attestation in enumerate(attestations, 1):
message += f"{i}. {attestation}\n"
message += f" {i}. {org} {attestation}\n"
message += f"""
Proofs:
NIST time: {nist_time}
Democracy Now! headline: "{democracy_now_headline}"
Bitcoin block #{bitcoin_block['height']} hash: {bitcoin_block['hash']}
Bitcoin block time: {bitcoin_block['time']}
"""
return message
if note:
message += f"\nNOTE: {note}\n"
message += "\nDatestamp Proof:\n"
message += f" Daily News: \"{rss_data['title']}\"\n"
message += f" Source URL: {rss_data['link']}\n"
message += f" BTC block: #{bitcoin_block['height']}, {bitcoin_block['time']}\n"
message += f" Block hash: {bitcoin_block['hash']}\n"
return message.rstrip() + "\n" # Single newline before signature
def sign_with_gpg(message, gpg_key_id):
"""Sign the warrant canary message with GPG."""
"""Sign the warrant canary message with GPG, ensuring no extra newline after signature header."""
try:
# Write message to temporary file
with open(TEMP_MESSAGE_FILE, "w") as f:
with open(TEMP_MESSAGE_FILE, "w", newline='\n') as f: # Unix line endings
f.write(message)
# Sign the message with GPG
cmd = ["gpg", "--clearsign", "--default-key", gpg_key_id, TEMP_MESSAGE_FILE]
subprocess.run(cmd, check=True)
# Read the signed message
with open(f"{TEMP_MESSAGE_FILE}.asc", "r") as f:
signed_message = f.read()
# Clean up temporary files
os.remove(TEMP_MESSAGE_FILE)
os.remove(f"{TEMP_MESSAGE_FILE}.asc")
# Fix GPG's extra newline after -----BEGIN PGP SIGNATURE-----
lines = signed_message.splitlines()
signature_idx = next(i for i, line in enumerate(lines) if line == "-----BEGIN PGP SIGNATURE-----")
if lines[signature_idx + 1] == "":
lines.pop(signature_idx + 1) # Remove blank line
signed_message = "\n".join(lines)
return signed_message
except subprocess.CalledProcessError as e:
print(f"GPG signing error: {e}")
@ -215,117 +201,65 @@ def save_warrant_canary(signed_message):
return False
async def post_to_matrix(config, signed_message):
"""Post the signed warrant canary to Matrix room using nio library."""
"""Post the signed warrant canary to Matrix room."""
if not config.get('matrix', {}).get('enabled', False):
print("Matrix posting is disabled in config")
return False
try:
from nio import AsyncClient, LoginResponse
matrix = config['matrix']
client = AsyncClient(matrix['homeserver'], matrix['username'])
await client.login(matrix['password'])
# Get Matrix config
homeserver = config['matrix']['homeserver']
username = config['matrix']['username']
password = config['matrix']['password']
room_id = config['matrix']['room_id']
full_message = (
f"This is the {config['canary']['organization']} Warrant Canary, signed with GPG for authenticity. "
"Copy the code block below to verify with `gpg --verify`:\n\n"
f"```\n{signed_message}\n```"
)
# Extract username without domain for login
user_id = username
if username.startswith('@'):
user_id = username[1:] # Remove @ prefix
if ':' in user_id:
user_id = user_id.split(':')[0] # Remove server part
# Create client
client = AsyncClient(homeserver, username)
# Login
print(f"Logging in as {username} on {homeserver}...")
response = await client.login(password)
if isinstance(response, LoginResponse):
print("Login successful")
else:
print(f"Matrix login failed: {response}")
await client.close()
return False
# Format message for Matrix
print(f"Posting canary to room {room_id}...")
try:
# Use HTML formatting for the message
content = {
"msgtype": "m.text",
"body": signed_message, # Plain text version
"format": "org.matrix.custom.html",
"formatted_body": f"<pre>{signed_message}</pre>" # HTML version with preformatted text
}
response = await client.room_send(
room_id=room_id,
message_type="m.room.message",
content=content
content = {
"msgtype": "m.text",
"body": full_message,
"format": "org.matrix.custom.html",
"formatted_body": (
f"This is the {config['canary']['organization']} Warrant Canary, signed with GPG for authenticity. "
"Copy the code block below to verify with <code>gpg --verify</code>:<br><br>"
f"<pre>{signed_message}</pre>"
)
print("Successfully posted warrant canary to Matrix room")
except Exception as e:
print(f"Error sending message: {e}")
await client.close()
return False
# Logout and close
}
await client.room_send(matrix['room_id'], "m.room.message", content)
await client.logout()
await client.close()
print("Posted to Matrix successfully")
return True
except ImportError:
print("Error: matrix-nio library not installed. Install with: pip install matrix-nio")
return False
except Exception as e:
print(f"Error posting to Matrix: {e}")
return False
def main():
print("Generating We2.ee warrant canary...")
# Load configuration
print("Generating warrant canary...")
config = load_config()
gpg_key_id = config['gpg']['key_id']
# Create message
message = create_warrant_canary_message(config)
if not message:
print("Failed to create warrant canary message")
print("Failed to create message")
sys.exit(1)
# Display the message
print("\nWarrant Canary Message Preview:")
print("\nWarrant Canary Preview:")
print("-" * 50)
print(message)
print("-" * 50)
# Confirm with user
user_input = input("\nDo you want to sign this message with GPG? (y/n): ")
if user_input.lower() != 'y':
if input("\nSign with GPG? (y/n): ").lower() != 'y':
print("Operation cancelled")
sys.exit(0)
# Sign and save
signed_message = sign_with_gpg(message, gpg_key_id)
signed_message = sign_with_gpg(message, config['gpg']['key_id'])
if not signed_message:
print("Failed to sign warrant canary message")
print("Failed to sign message")
sys.exit(1)
if save_warrant_canary(signed_message):
print("Warrant canary generated successfully!")
else:
print("Failed to save warrant canary")
sys.exit(1)
# Post to Matrix if enabled
if config.get('matrix', {}).get('enabled', False):
post_to_matrix_input = input("\nDo you want to post the warrant canary to Matrix? (y/n): ")
if post_to_matrix_input.lower() == 'y':
if save_warrant_canary(signed_message) and config.get('matrix', {}).get('enabled', False):
if input("Post to Matrix? (y/n): ").lower() == 'y':
asyncio.run(post_to_matrix(config, signed_message))
if __name__ == "__main__":

View file

@ -1,34 +1,30 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
We2.ee Warrant Canary
Date: 2025-03-30
1. We have not received any National Security Letters.
2. We have not received any court orders under the Foreign Intelligence Surveillance Act.
3. We have not received any gag orders that prevent us from stating we have received legal process.
4. We have not been required to modify our systems to facilitate surveillance.
5. We have not been subject to any searches or seizures of our servers.
Proofs:
NIST time: 2025-03-30T03:15:24.5369625 UTC
Democracy Now! headline: ""The Encampments": New Film on Mahmoud Khalil & Columbia Students Who Sparked Gaza Campus Protests"
Bitcoin block #890061 hash: 000000000000000000022c41b8bf19607d604f9b77d0403439bbf6ee60215332
Bitcoin block time: 2025-03-30 03:10:49 UTC
We2.ee Warrant Canary · 2025-03-30T11:43:29.0985236 UTC
I, 〄, the server administrator of We2.ee, state this 30th day of March, 2025:
1. We2.ee has not received any National Security Letters or FISA court orders.
2. We2.ee has not been subject to any searches or seizures of our servers.
3. We2.ee has not been required to modify our systems to facilitate surveillance.
4. We2.ee has not received any gag order that restrain our ability to make these declarations truthfully.
Datestamp Proof:
Daily News: ""The Encampments": New Film on Mahmoud Khalil & Columbia Students Who Sparked Gaza Campus Protests"
Source URL: http://www.democracynow.org/2025/3/28/the_encampments_columbia_university_documentary
BTC block: #890107, 2025-03-30 11:39:28 UTC
Block hash: 362a1d72055578b915f653a99da326bee7c664d8b158
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCgAdFiEEMjqKLEezdiJLNhO3U1smWu2+W0QFAmfot2QACgkQU1smWu2+
W0SHBRAAhYrE5ZaBU+J/ixjK1GATqEqgV20weirjnUvlfqyvH6lhBF8xE7EclR/K
7lNvaZlgqF3ks6NcMs02h74wrXhQeWfJ9QUDMjcsQkz1OZAHylG7T6dzzizz0MXM
ldxL2D8sXFVLN78wNxDFpKc7PnWkbEsAqD/OXLDQUDDwphufsvZAvXhBuGvPxYvF
I7J69LPO2nZfgTnxmxP/xtsiAFQ2HB5WjNzyt1JuK5Jnx/cZ8+plUa2+2GhdQ9Me
8bMFVrsHiTRGZH3uCW+ndETJEGNpXbA50iE0trLvsb3BZXSS9YN9vmd9O+psFEI4
zDlZKbiuqJ/s2A00zTNb0/ZVfd3C4OwjSX6SKghq7ABt4W38FmbqPipExsNqCMpa
NrTPQCuSRZ1Lpfvt4QtqIGRdFVtcO+RCMbeTQpdsuy/3rm1Wu1PDbO6EMdCFo4/I
7b+QnW6CFQZlKe/Tt2aTc/3cKb50LZO9+Zd5eKfkX2lXlcNuUHc9+Qts1OTf58nk
sRdl2WdGpjXFlC5dnQ8+BvRl7m3QROl44bo/jx1krfCtzZj8YQkFQQnwdj3cWXMk
Pkz9bMMZ6IbyDEKy/y/8GHblvNBzcuSfSY8aoEc2mdESh7uURKQBkpkDpFg6dYS3
b0lkabVcMXUj8nVJEtnAXcjkVWblGM0OYNSg+EpxGhvGQ25Gy8w=
=ElP1
-----END PGP SIGNATURE-----
iQIzBAEBCgAdFiEEMjqKLEezdiJLNhO3U1smWu2+W0QFAmfpLpUACgkQU1smWu2+
W0StRw/8Cp6zZVvoVfyyWEmGyTbXbbYCmpHcHyyB+17ZZQPu3ZVHCrS58e0JTcb/
x+NF/0QfBpwsmNJk5qWqwNkdoZLc26tW47XcyzLAs1f9OoKsFf/OIFkggL2AUtoS
t+vZ/e2pW/5YTud9L4K2CrKReuEb/yzXE5PVqqN/OYwYXUzb0LvNub7o+DtVWckD
tJueplKr9jmDgxRXZ/9RBDvsJtsnQynYkFnPxHodBNCaMClshIr/XrPKT7uB97k2
n/Q3JNAMlwGSO2cKRGIhBlTshnNOeNZo+7cOA/zKbUhpZxOIb3OeQvt4exhcM4WE
ezubOMJMfd9h4IgtPY7/ZxbP+k1OFzHV6hP39xFbmgpsdPK9mnWRlvPR3Hhfzo/B
DNh0fn4JS8GLCO9SujGqJ81Pj6l3g0HzMbsdu8zDxfz2CiV4FGpOWJ1r/Gw8rKtX
J5ptp0iUbpmznDDZwHLlq9nLdYOcnbZX5PHKksbZ86H//1AMvYN9GWaT56PwCJbK
eH5o23u8HnZom0mmDS0OPOAmImNoQKkhVnnECqyrg6lPsgXGc6B7+e+fkeoQz6vy
taiq7yaumyKT8mQ3YmRiGQ4pI9wTFFQsHt5p4jZ02RAWZGMTolurlLVHhVFWEsyc
03Xm5Zzb1yfh/kWcHxu5r4uVdhCDx2W1gjE9eUKQW3EJPDS+Kn0=
=fj1f
-----END PGP SIGNATURE-----

View file

@ -8,8 +8,7 @@ CONDUWUIT_REGISTRATION_TOKEN_FILE=/.registration_token
CONDUWUIT_ADDRESS=["0.0.0.0", "::"]
CONDUWUIT_PORT=8008
CONDUWUIT_NEW_USER_DISPLAYNAME_SUFFIX=
CONDUWUIT_AUTO_JOIN_ROOMS=["#Home:we2.ee", "#Announcements:we2.ee"]
CONDUWUIT_FORGET_FORCED_UPON_LEAVE=true
CONDUWUIT_AUTO_JOIN_ROOMS=["#home:we2.ee", "#server:we2.ee", "#pub:we2.ee", "#help:we2.ee"]
CONDUWUIT_DB_CACHE_CAPACITY_MB=1024
CONDUWUIT_DB_WRITE_BUFFER_CAPACITY_MB=256
CONDUWUIT_DB_POOL_WORKERS=64
@ -21,16 +20,16 @@ CONDUWUIT_ALLOW_FEDERATION=true
CONDUWUIT_ALLOW_PUBLIC_ROOM_DIRECTORY_OVER_FEDERATION=true
CONDUWUIT_ALLOW_PUBLIC_ROOM_DIRECTORY_WITHOUT_AUTH=true
CONDUWUIT_WELL_KNOWN_CONN_TIMEOUT=30
CONDUWUIT_FEDERATION_TIMEOUT=600
CONDUWUIT_FEDERATION_TIMEOUT=60
CONDUWUIT_FEDERATION_IDLE_TIMEOUT=60
CONDUWUIT_SENDER_TIMEOUT=600
CONDUWUIT_SENDER_IDLE_TIMEOUT=360
CONDUWUIT_SENDER_IDLE_TIMEOUT=300
CONDUWUIT_SENDER_SHUTDOWN_TIMEOUT=30
CONDUWUIT_DNS_CACHE_ENTRIES=0
CONDUWUIT_DNS_MIN_TTL=0
CONDUWUIT_DNS_MIN_TTL_NXDOMAIN=60
CONDUWUIT_DNS_ATTEMPTS=3
CONDUWUIT_DNS_TIMEOUT=3
CONDUWUIT_DNS_MIN_TTL_NXDOMAIN=15
CONDUWUIT_DNS_ATTEMPTS=5
CONDUWUIT_DNS_TIMEOUT=5
CONDUWUIT_DNS_TCP_FALLBACK=true
CONDUWUIT_QUERY_ALL_NAMESERVERS=false
CONDUWUIT_QUERY_OVER_TCP_ONLY=false

View file

@ -7,6 +7,7 @@ LOG_FILE="$BASE_PATH/token_refresh.log"
BACKUP_PATH="/home/sij/conduwuit_backup"
ENV_FILE="$BASE_PATH/conduwuit.env"
REPO_PATH="$HOME/workshop/conduwuit"
CONFIG_FILE="$BASE_PATH/config.yaml"
# Static container settings
CONTAINER_NAME="conduwuit"
@ -16,8 +17,9 @@ CONTAINER_IMAGE="conduwuit:custom"
REFRESH_TOKEN=false
SUPER_ADMIN=false
UPDATE=false
FORCE_RESTART=false
# Function to log with timestamp to both file and terminal
# Function to log with a timestamp to both file and terminal
log() {
local message="$(date --iso-8601=seconds) $1"
echo "$message" >> "$LOG_FILE" # Write to log file
@ -142,9 +144,10 @@ restart_container() {
fi
}
# Function to start the Python registration service
start_registration_service() {
local python_script="$BASE_PATH/registration.py" # Adjust name if different
# Function to ensure the registration service is running.
# If --force-restart is passed, it will forcefully kill any process listening on the registration port.
ensure_registration_service() {
local python_script="$BASE_PATH/registration.py" # Adjust name if needed
local pid_file="$BASE_PATH/registration.pid"
local log_file="$BASE_PATH/registration.log"
@ -153,15 +156,36 @@ start_registration_service() {
exit 1
fi
# Check if it's already running
if [ -f "$pid_file" ] && ps -p "$(cat "$pid_file")" > /dev/null 2>&1; then
log "Registration service already running with PID $(cat "$pid_file")"
else
# Start it in the background, redirecting output to a log file
# Retrieve the port from the config file (default to 8000 if not specified)
REG_PORT=$(python3 -c "import yaml, sys; print(yaml.safe_load(open('$CONFIG_FILE')).get('port', 8000))")
log "Registration service port from config: $REG_PORT"
if [ "$FORCE_RESTART" = true ]; then
log "Force restart requested. Clearing any process listening on port $REG_PORT..."
PIDS=$(lsof -ti tcp:"$REG_PORT")
if [ -n "$PIDS" ]; then
kill -9 $PIDS && log "Killed processes: $PIDS" || log "Failed to kill process(es) on port $REG_PORT"
else
log "No process found running on port $REG_PORT"
fi
rm -f "$pid_file"
log "Force starting registration service..."
python3 "$python_script" >> "$log_file" 2>&1 &
local pid=$!
echo "$pid" > "$pid_file"
log "Started registration service with PID $pid"
NEW_PID=$!
echo "$NEW_PID" > "$pid_file"
log "Started registration service with PID $NEW_PID"
else
# Check if there is any process already listening on the port
EXISTING_PIDS=$(lsof -ti tcp:"$REG_PORT")
if [ -n "$EXISTING_PIDS" ]; then
log "Registration service already running on port $REG_PORT with PID(s): $EXISTING_PIDS"
else
log "Registration service not running on port $REG_PORT, starting..."
python3 "$python_script" >> "$log_file" 2>&1 &
NEW_PID=$!
echo "$NEW_PID" > "$pid_file"
log "Started registration service with PID $NEW_PID"
fi
fi
}
@ -180,13 +204,13 @@ while [[ $# -gt 0 ]]; do
UPDATE=true
shift
;;
--start-service)
START_SERVICE=true
--force-restart)
FORCE_RESTART=true
shift
;;
*)
log "ERROR: Unknown option: $1"
echo "Usage: $0 [--refresh-token] [--super-admin] [--update]"
echo "Usage: $0 [--refresh-token] [--super-admin] [--update] [--force-restart]"
exit 1
;;
esac
@ -200,8 +224,8 @@ if [ "$REFRESH_TOKEN" = true ]; then
refresh_token
fi
restart_container
if [ "$START_SERVICE" = true ] || [ "$1" = "@reboot" ]; then # Run on explicit flag or cron @reboot
start_registration_service
fi
# Always ensure the registration service is running.
ensure_registration_service
exit 0

View file

@ -1 +1 @@
749006
3393774

172
registration.py Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import os
import re
import yaml
@ -228,7 +229,8 @@ def check_email_cooldown(email: str) -> Optional[str]:
if not config.get("multiple_users_per_email", True):
return "This email address has already been used to register an account."
if email_cooldown := config.get("email_cooldown"):
email_cooldown = config.get("email_cooldown")
if email_cooldown:
latest_registration = max(
datetime.fromisoformat(e["datetime"])
for e in email_entries
@ -269,7 +271,68 @@ async def check_username_availability(username: str) -> bool:
return False
# ---------------------------------------------------------
# 5. FastAPI Setup and Routes
# 5. Email Helper Functions
# ---------------------------------------------------------
def build_email_message(token: str, requested_username: str, now: datetime, recipient_email: str) -> EmailMessage:
"""
Build and return an EmailMessage for registration.
"""
time_until_reset = get_time_until_reset_str(now)
# Format bodies using config templates
plain_body = config["email_body"].format(
homeserver=config["homeserver"],
registration_token=token,
requested_username=requested_username,
utc_time=now.strftime("%H:%M:%S"),
time_until_reset=time_until_reset
)
html_body = config.get("email_body_html", "").format(
homeserver=config["homeserver"],
registration_token=token,
requested_username=requested_username,
utc_time=now.strftime("%H:%M:%S"),
time_until_reset=time_until_reset
)
msg = EmailMessage()
msg.set_content(plain_body)
if html_body:
msg.add_alternative(html_body, subtype="html")
msg["Subject"] = config["email_subject"].format(homeserver=config["homeserver"])
# Get the sender value from configuration.
# Ensure it's fully-qualified: it must contain an "@".
from_value = config["smtp"].get("from")
if not from_value or "@" not in from_value:
logger.warning(f"Sender address '{from_value}' is not fully-qualified. Falling back to {config['smtp']['username']}.")
from_value = config["smtp"]["username"]
msg["From"] = from_value
msg["To"] = recipient_email
return msg
def send_email_message(msg: EmailMessage) -> None:
"""
Send an email message using SMTP configuration.
"""
smtp_conf = config["smtp"]
try:
with smtplib.SMTP(smtp_conf["host"], smtp_conf["port"]) as server:
if smtp_conf.get("use_tls", True):
server.starttls()
server.login(smtp_conf["username"], smtp_conf["password"])
server.send_message(msg)
logger.info(f"Registration email sent successfully to {msg['To']}")
except Exception as ex:
logger.error(f"Failed to send email: {ex}")
raise HTTPException(status_code=500, detail=f"Error sending email: {ex}")
# ---------------------------------------------------------
# 6. FastAPI Setup and Routes
# ---------------------------------------------------------
app = FastAPI()
app.add_middleware(CustomLoggingMiddleware)
@ -307,109 +370,41 @@ async def register(
):
now = get_current_utc()
client_ip = request.client.host
logger.info(f"Registration attempt - Username: {requested_username}, Email: {email}, IP: {client_ip}")
closed, message = is_registration_closed(now)
if closed:
logger.info("Registration rejected: Registration is closed")
return templates.TemplateResponse(
"error.html",
{
"request": request,
"message": message
}
)
return templates.TemplateResponse("error.html", {"request": request, "message": message})
if is_ip_banned(client_ip):
logger.info(f"Registration rejected: Banned IP {client_ip}")
return templates.TemplateResponse(
"error.html",
{
"request": request,
"message": "Registration not allowed from your IP address."
}
)
return templates.TemplateResponse("error.html", {"request": request, "message": "Registration not allowed from your IP address."})
if is_email_banned(email):
logger.info(f"Registration rejected: Banned email {email}")
return templates.TemplateResponse(
"error.html",
{
"request": request,
"message": "Registration not allowed for this email address."
}
)
return templates.TemplateResponse("error.html", {"request": request, "message": "Registration not allowed for this email address."})
if error_message := check_email_cooldown(email):
logger.info(f"Registration rejected: Email cooldown - {email}")
return templates.TemplateResponse(
"error.html",
{
"request": request,
"message": error_message
}
)
return templates.TemplateResponse("error.html", {"request": request, "message": error_message})
available = await check_username_availability(requested_username)
if not available:
logger.info(f"Registration rejected: Username unavailable - {requested_username}")
return templates.TemplateResponse(
"error.html",
{
"request": request,
"message": f"The username '{requested_username}' is not available."
}
)
return templates.TemplateResponse("error.html", {"request": request, "message": f"The username '{requested_username}' is not available."})
token = read_registration_token()
if token is None:
logger.error("Registration token file not found")
raise HTTPException(status_code=500, detail="Registration token file not found.")
time_until_reset = get_time_until_reset_str(now)
# Plain text email body
email_body = config["email_body"].format(
homeserver=config["homeserver"],
registration_token=token,
requested_username=requested_username,
utc_time=now.strftime("%H:%M:%S"),
time_until_reset=time_until_reset
)
# Build and send registration email
email_message = build_email_message(token, requested_username, now, email)
send_email_message(email_message)
# HTML email body
email_body_html = config.get("email_body_html", "").format(
homeserver=config["homeserver"],
registration_token=token,
requested_username=requested_username,
utc_time=now.strftime("%H:%M:%S"),
time_until_reset=time_until_reset
)
msg = EmailMessage()
msg.set_content(email_body)
# Add HTML version if configured
if email_body_html:
msg.add_alternative(email_body_html, subtype='html')
msg["Subject"] = config["email_subject"].format(homeserver=config["homeserver"])
msg["From"] = config["smtp"]["username"]
msg["To"] = email
try:
smtp_conf = config["smtp"]
with smtplib.SMTP(smtp_conf["host"], smtp_conf["port"]) as server:
if smtp_conf.get("use_tls", True):
server.starttls()
server.login(smtp_conf["username"], smtp_conf["password"])
server.send_message(msg)
logger.info(f"Registration email sent successfully to {email}")
except Exception as ex:
logger.error(f"Failed to send email: {ex}")
raise HTTPException(status_code=500, detail=f"Error sending email: {ex}")
# Save registration data and log success
registration_data = {
"requested_name": requested_username,
"email": email,
@ -417,16 +412,9 @@ async def register(
"ip_address": client_ip
}
save_registration(registration_data)
logger.info(f"Registration successful - Username: {requested_username}, Email: {email}")
return templates.TemplateResponse(
"success.html",
{
"request": request,
"homeserver": config["homeserver"]
}
)
return templates.TemplateResponse("success.html", {"request": request, "homeserver": config["homeserver"]})
if __name__ == "__main__":
import uvicorn