Warrant canary, better registration service management
This commit is contained in:
parent
90242a495e
commit
4ce1ff8cfd
7 changed files with 265 additions and 323 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
286
canary.py
Normal file → Executable 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__":
|
||||
|
|
54
canary.txt
54
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-----
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
749006
|
||||
3393774
|
||||
|
|
172
registration.py
Normal file → Executable file
172
registration.py
Normal file → Executable 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
|
||||
|
|
Loading…
Add table
Reference in a new issue