from bottle import route, request, response, run, static_file, template import hashlib import time import json import os import re from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC import base64 import sqlalchemy from sqlalchemy import create_engine, String, Integer, Text from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session from sqlalchemy.orm import sessionmaker # --- Security config from environment --- SALT = os.environ.get('CHAT_SALT', 'default_salt_change_me').encode() PASSWORD = os.environ.get('CHAT_SECRET', 'default_secret_change_me').encode() ADMIN_TOKEN = os.environ.get('CHAT_ADMIN_TOKEN', None) RATE_LIMIT = int(os.environ.get('CHAT_RATE_LIMIT', 5)) # messages per window RATE_WINDOW = int(os.environ.get('CHAT_RATE_WINDOW', 10)) # seconds MAX_MSG_LENGTH = int(os.environ.get('CHAT_MAX_MSG', 2000)) DB_PATH = os.environ.get('CHAT_DB_PATH', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chat.db')) # --- Rate limiting (in-memory) --- rate_store = {} def check_rate_limit(ip): now = time.time() window_start = now - RATE_WINDOW if ip in rate_store: rate_store[ip] = [t for t in rate_store[ip] if t > window_start] if len(rate_store[ip]) >= RATE_LIMIT: return False rate_store[ip].append(now) else: rate_store[ip] = [now] return True # --- Encryption --- def get_cipher(): kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=SALT, iterations=100000, ) key = base64.urlsafe_b64encode(kdf.derive(PASSWORD)) return Fernet(key) class Base(DeclarativeBase): pass class Messages(Base): __tablename__ = "messages" id: Mapped[int] = mapped_column(Integer, primary_key=True) channel: Mapped[str] = mapped_column(String(32), nullable=False, default='general') ip: Mapped[str] = mapped_column(String(64), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) timestamp: Mapped[int] = mapped_column(Integer, nullable=False) engine = create_engine(f"sqlite:///{DB_PATH}", echo=False) Base.metadata.create_all(engine) class DBfrontend(): def __init__(self): self.Session = sessionmaker(bind=engine) self.cipher = get_cipher() def _encrypt(self, message): return self.cipher.encrypt(message.encode()).decode() def _decrypt(self, encrypted): try: return self.cipher.decrypt(encrypted.encode()).decode() except: return "[Decryption failed]" def add_message(self, channel, ip, content): with self.Session() as session: msg = Messages( channel=channel, ip=ip, content=self._encrypt(content), timestamp=int(time.time() * 1000) ) session.add(msg) session.commit() return msg.id def get_messages(self, channel='general', limit=50): with self.Session() as session: q = session.query(Messages).filter(Messages.channel == channel) q = q.order_by(Messages.id.desc()).limit(limit) msgs = q.all() result = [] for msg in reversed(msgs): result.append({ 'id': msg.id, 'channel': msg.channel, 'ip': msg.ip, 'content': self._decrypt(msg.content), 'timestamp': msg.timestamp }) return result def delete_message(self, msg_id): with self.Session() as session: msg = session.query(Messages).get(msg_id) if msg: session.delete(msg) session.commit() return True return False db = DBfrontend() VALID_CHANNELS = {'general', 'games', 'anime', 'politics'} CHANNEL_NAMES = { 'general': 'General', 'games': 'Games', 'anime': 'Anime', 'politics': 'Politics' } @route('/') def serve_frontend(): return static_file('index.html', root=os.path.dirname(os.path.abspath(__file__))) @route('/api/channels', method='GET') def list_channels(): response.content_type = 'application/json' channels = [{'id': k, 'name': v} for k, v in CHANNEL_NAMES.items()] return {'success': True, 'channels': channels} @route('/api/messages', method='POST') def post_message(): ip = request.environ.get('REMOTE_ADDR', '127.0.0.1') if not check_rate_limit(ip): response.status = 429 return {'success': False, 'error': 'Rate limit exceeded. Slow down.'} try: data = request.json if not data: response.status = 400 return {'success': False, 'error': 'No data provided'} channel = data.get('channel', 'general') content = data.get('message', '').strip() if channel not in VALID_CHANNELS: response.status = 400 return {'success': False, 'error': 'Invalid channel'} if not content: response.status = 400 return {'success': False, 'error': 'Empty message'} if len(content) > MAX_MSG_LENGTH: response.status = 400 return {'success': False, 'error': f'Message too long (max {MAX_MSG_LENGTH} chars)'} # XSS filter - strip HTML tags content = re.sub(r'<[^>]*>', '', content) ip_hash = hashlib.sha256((os.environ.get('CHAT_SALT', 'salt') + ip).encode()).hexdigest()[:16] msg_id = db.add_message(channel, ip_hash, content) response.content_type = 'application/json' return {'success': True, 'message_id': msg_id, 'ip': ip_hash} except Exception as e: response.status = 500 return {'success': False, 'error': str(e)} @route('/api/messages', method='GET') def get_messages(): try: channel = request.query.get('channel', 'general') limit = min(int(request.query.get('limit', 50)), 200) if channel not in VALID_CHANNELS: channel = 'general' messages = db.get_messages(channel=channel, limit=limit) response.content_type = 'application/json' return {'success': True, 'messages': messages} except Exception as e: response.status = 500 return {'success': False, 'error': str(e)} @route('/api/messages/', method='DELETE') def delete_message(message_id): # Admin token required for deletion auth = request.get_header('Authorization', '') token = auth.replace('Bearer ', '') if ADMIN_TOKEN and token != ADMIN_TOKEN: response.status = 403 return {'success': False, 'error': 'Forbidden'} try: success = db.delete_message(int(message_id)) response.content_type = 'application/json' return {'success': success} except Exception as e: response.status = 500 return {'success': False, 'error': str(e)} if __name__ == '__main__': host = os.environ.get('CHAT_HOST', 'localhost') port = int(os.environ.get('CHAT_PORT', 8080)) debug_mode = os.environ.get('CHAT_DEBUG', '0') == '1' if debug_mode: print(f"⚠️ WARNING: Debug mode is ON - do not use in production!") run(host=host, port=port, debug=debug_mode)