diff --git a/.gitignore b/.gitignore index fe9c265..95c5b4d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ registrations.json banned_ips.txt refresh_token.sh testbench/ +registration.pid # Backup directories backup/ diff --git a/canary.py b/canary.py old mode 100644 new mode 100755 index db744f5..3ed5fef --- a/canary.py +++ b/canary.py @@ -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__": diff --git a/canary.txt b/canary.txt index 6c77d4d..2180626 100644 --- a/canary.txt +++ b/canary.txt @@ -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----- \ No newline at end of file diff --git a/conduwuit.env b/conduwuit.env index cfbf998..711e624 100644 --- a/conduwuit.env +++ b/conduwuit.env @@ -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 diff --git a/launch_conduwuit.sh b/launch_conduwuit.sh index 7061c40..f0371ff 100755 --- a/launch_conduwuit.sh +++ b/launch_conduwuit.sh @@ -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 diff --git a/registration.pid b/registration.pid index 394b6b6..ea95ed4 100644 --- a/registration.pid +++ b/registration.pid @@ -1 +1 @@ -749006 +3393774 diff --git a/registration.py b/registration.py old mode 100644 new mode 100755 index dcb0e93..626574e --- a/registration.py +++ b/registration.py @@ -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